blob: dd8b8d0ab5d6aff92dffc67e2fd7bc69c0fba947 [file] [log] [blame]
Harald Welteb2edd142021-01-08 23:29:35 +01001#!/usr/bin/env python3
2
3# Interactive shell for working with SIM / UICC / USIM / ISIM cards
4#
5# (C) 2021 by Harald Welte <laforge@osmocom.org>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20from typing import List
21
22import json
Philipp Maier5d698e52021-09-16 13:18:01 +020023import traceback
Harald Welteb2edd142021-01-08 23:29:35 +010024
25import cmd2
Vadim Yanitskiy0d9f0882022-07-14 19:08:24 +070026from cmd2 import style, fg
Harald Welteb2edd142021-01-08 23:29:35 +010027from cmd2 import CommandSet, with_default_category, with_argparser
28import argparse
29
30import os
31import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010032from pathlib import Path
Philipp Maier76667642021-09-22 16:53:22 +020033from io import StringIO
Harald Welteb2edd142021-01-08 23:29:35 +010034
Harald Welteb2edd142021-01-08 23:29:35 +010035from pySim.exceptions import *
36from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020037from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020038from pySim.cards import card_detect, SimCard
Harald Welte1e52b0d2022-07-16 11:53:21 +020039from pySim.utils import h2b, swap_nibbles, rpad, b2h, JsonEncoder, bertlv_parse_one, sw_match
40from pySim.utils import sanitize_pin_adm, tabulate_str_list, boxed_heading_str
Philipp Maier76667642021-09-22 16:53:22 +020041from pySim.card_handler import CardHandler, CardHandlerAuto
Harald Welteb2edd142021-01-08 23:29:35 +010042
Harald Welte1e52b0d2022-07-16 11:53:21 +020043from pySim.filesystem import RuntimeState, CardDF, CardADF, CardModel
Philipp Maiera028c7d2021-11-08 16:12:03 +010044from pySim.profile import CardProfile
Harald Welteb2edd142021-01-08 23:29:35 +010045from pySim.ts_102_221 import CardProfileUICC
Harald Welte3c9b7842021-10-19 21:44:24 +020046from pySim.ts_102_222 import Ts102222Commands
Harald Welte5ce35242021-04-02 20:27:05 +020047from pySim.ts_31_102 import CardApplicationUSIM
48from pySim.ts_31_103 import CardApplicationISIM
Harald Welte95ce6b12021-10-20 18:40:54 +020049from pySim.ara_m import CardApplicationARAM
Harald Welte34eb5042022-02-21 17:19:28 +010050from pySim.global_platform import CardApplicationISD
Harald Welte2a33ad22021-10-20 10:14:18 +020051from pySim.gsm_r import DF_EIRENE
Harald Welteb2edd142021-01-08 23:29:35 +010052
Harald Welte4c1dca02021-10-14 17:48:25 +020053# we need to import this module so that the SysmocomSJA2 sub-class of
54# CardModel is created, which will add the ATR-based matching and
55# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020056import pySim.sysmocom_sja2
57
Harald Welte4442b3d2021-04-03 09:00:16 +020058from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010059
Harald Weltec91085e2022-02-10 18:05:45 +010060
Philipp Maierea95c392021-09-16 13:10:19 +020061def init_card(sl):
Harald Weltec91085e2022-02-10 18:05:45 +010062 """
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 """
Philipp Maierea95c392021-09-16 13:10:19 +020067
Harald Weltec91085e2022-02-10 18:05:45 +010068 # 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
Philipp Maierea95c392021-09-16 13:10:19 +020079
Philipp Maier24031252022-06-14 16:16:42 +020080 generic_card = False
Harald Weltec91085e2022-02-10 18:05:45 +010081 card = card_detect("auto", scc)
82 if card is None:
83 print("Warning: Could not detect card type - assuming a generic card type...")
84 card = SimCard(scc)
Philipp Maier24031252022-06-14 16:16:42 +020085 generic_card = True
Philipp Maiera028c7d2021-11-08 16:12:03 +010086
Harald Weltec91085e2022-02-10 18:05:45 +010087 profile = CardProfile.pick(scc)
88 if profile is None:
89 print("Unsupported card type!")
Philipp Maier7226c092022-06-01 17:58:38 +020090 return None, card
Philipp Maierea95c392021-09-16 13:10:19 +020091
Philipp Maier24031252022-06-14 16:16:42 +020092 # ETSI TS 102 221, Table 9.3 specifies a default for the PIN key
93 # references, however card manufactures may still decide to pick an
94 # arbitrary key reference. In case we run on a generic card class that is
95 # detected as an UICC, we will pick the key reference that is officially
96 # specified.
97 if generic_card and isinstance(profile, CardProfileUICC):
98 card._adm_chv_num = 0x0A
99
Harald Weltec91085e2022-02-10 18:05:45 +0100100 print("Info: Card is of type: %s" % str(profile))
Philipp Maiera028c7d2021-11-08 16:12:03 +0100101
Harald Weltec91085e2022-02-10 18:05:45 +0100102 # FIXME: This shouln't be here, the profile should add the applications,
103 # however, we cannot simply put his into ts_102_221.py since we would
104 # have to e.g. import CardApplicationUSIM from ts_31_102.py, which already
105 # imports from ts_102_221.py. This means we will end up with a circular
106 # import, which needs to be resolved first.
107 if isinstance(profile, CardProfileUICC):
108 profile.add_application(CardApplicationUSIM())
109 profile.add_application(CardApplicationISIM())
110 profile.add_application(CardApplicationARAM())
Harald Welte34eb5042022-02-21 17:19:28 +0100111 profile.add_application(CardApplicationISD())
Philipp Maiera028c7d2021-11-08 16:12:03 +0100112
Harald Weltec91085e2022-02-10 18:05:45 +0100113 # Create runtime state with card profile
114 rs = RuntimeState(card, profile)
Philipp Maierea95c392021-09-16 13:10:19 +0200115
Harald Weltec91085e2022-02-10 18:05:45 +0100116 # FIXME: This is an GSM-R related file, it needs to be added throughout,
117 # the profile. At the moment we add it for all cards, this won't hurt,
118 # but regular SIM and UICC will not have it and fail to select it.
119 rs.mf.add_file(DF_EIRENE())
Philipp Maierea95c392021-09-16 13:10:19 +0200120
Harald Weltec91085e2022-02-10 18:05:45 +0100121 CardModel.apply_matching_models(scc, rs)
Harald Weltef44256c2021-10-14 15:53:39 +0200122
Harald Weltec91085e2022-02-10 18:05:45 +0100123 # inform the transport that we can do context-specific SW interpretation
124 sl.set_sw_interpreter(rs)
Philipp Maierea95c392021-09-16 13:10:19 +0200125
Harald Weltec91085e2022-02-10 18:05:45 +0100126 return rs, card
127
Philipp Maier2b11c322021-03-17 12:37:39 +0100128
Harald Welteb2edd142021-01-08 23:29:35 +0100129class PysimApp(cmd2.Cmd):
Harald Weltec91085e2022-02-10 18:05:45 +0100130 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier76667642021-09-22 16:53:22 +0200131
Harald Weltec91085e2022-02-10 18:05:45 +0100132 def __init__(self, card, rs, sl, ch, script=None):
133 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
134 use_ipython=True, auto_load_commands=False, startup_script=script)
135 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
136 self.default_category = 'pySim-shell built-in commands'
137 self.card = None
138 self.rs = None
139 self.py_locals = {'card': self.card, 'rs': self.rs}
140 self.sl = sl
141 self.ch = ch
Harald Welte1748b932021-04-06 21:12:25 +0200142
Harald Weltec91085e2022-02-10 18:05:45 +0100143 self.numeric_path = False
144 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
145 onchange_cb=self._onchange_numeric_path))
146 self.conserve_write = True
147 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
148 onchange_cb=self._onchange_conserve_write))
149 self.json_pretty_print = True
150 self.add_settable(cmd2.Settable('json_pretty_print',
151 bool, 'Pretty-Print JSON output'))
152 self.apdu_trace = False
153 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
154 onchange_cb=self._onchange_apdu_trace))
Philipp Maier5d698e52021-09-16 13:18:01 +0200155
Harald Weltec91085e2022-02-10 18:05:45 +0100156 self.equip(card, rs)
Philipp Maier5d698e52021-09-16 13:18:01 +0200157
Harald Weltec91085e2022-02-10 18:05:45 +0100158 def equip(self, card, rs):
159 """
160 Equip pySim-shell with the supplied card and runtime state, add (or remove) all required settables and
161 and commands to enable card operations.
162 """
Philipp Maier76667642021-09-22 16:53:22 +0200163
Harald Weltec91085e2022-02-10 18:05:45 +0100164 rc = False
Philipp Maier5d698e52021-09-16 13:18:01 +0200165
Harald Weltec91085e2022-02-10 18:05:45 +0100166 # Unequip everything from pySim-shell that would not work in unequipped state
167 if self.rs:
168 self.rs.unregister_cmds(self)
169 for cmds in [Iso7816Commands, PySimCommands]:
170 cmd_set = self.find_commandsets(cmds)
171 if cmd_set:
172 self.unregister_command_set(cmd_set[0])
Philipp Maier5d698e52021-09-16 13:18:01 +0200173
Harald Weltec91085e2022-02-10 18:05:45 +0100174 self.card = card
175 self.rs = rs
Philipp Maier5d698e52021-09-16 13:18:01 +0200176
Harald Weltec91085e2022-02-10 18:05:45 +0100177 # When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
178 # needed to operate on cards.
179 if self.card and self.rs:
180 self._onchange_conserve_write(
181 'conserve_write', False, self.conserve_write)
182 self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
183 self.register_command_set(Iso7816Commands())
Harald Welte3c9b7842021-10-19 21:44:24 +0200184 self.register_command_set(Ts102222Commands())
Harald Weltec91085e2022-02-10 18:05:45 +0100185 self.register_command_set(PySimCommands())
186 self.iccid, sw = self.card.read_iccid()
187 rs.select('MF', self)
188 rc = True
189 else:
190 self.poutput("pySim-shell not equipped!")
Philipp Maier5d698e52021-09-16 13:18:01 +0200191
Harald Weltec91085e2022-02-10 18:05:45 +0100192 self.update_prompt()
193 return rc
Harald Welteb2edd142021-01-08 23:29:35 +0100194
Harald Weltec91085e2022-02-10 18:05:45 +0100195 def poutput_json(self, data, force_no_pretty=False):
196 """like cmd2.poutput() but for a JSON serializable dict."""
197 if force_no_pretty or self.json_pretty_print == False:
198 output = json.dumps(data, cls=JsonEncoder)
199 else:
200 output = json.dumps(data, cls=JsonEncoder, indent=4)
201 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100202
Harald Weltec91085e2022-02-10 18:05:45 +0100203 def _onchange_numeric_path(self, param_name, old, new):
204 self.update_prompt()
Philipp Maier38c74f62021-03-17 17:19:52 +0100205
Harald Weltec91085e2022-02-10 18:05:45 +0100206 def _onchange_conserve_write(self, param_name, old, new):
207 if self.rs:
208 self.rs.conserve_write = new
Harald Welte7829d8a2021-04-10 11:28:53 +0200209
Harald Weltec91085e2022-02-10 18:05:45 +0100210 def _onchange_apdu_trace(self, param_name, old, new):
211 if self.card:
212 if new == True:
213 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
214 else:
215 self.card._scc._tp.apdu_tracer = None
Harald Welte7829d8a2021-04-10 11:28:53 +0200216
Harald Weltec91085e2022-02-10 18:05:45 +0100217 class Cmd2ApduTracer(ApduTracer):
218 def __init__(self, cmd2_app):
219 self.cmd2 = app
Harald Welte7829d8a2021-04-10 11:28:53 +0200220
Harald Weltec91085e2022-02-10 18:05:45 +0100221 def trace_response(self, cmd, sw, resp):
222 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
223 self.cmd2.poutput("<- %s: %s" % (sw, resp))
Harald Welteb2edd142021-01-08 23:29:35 +0100224
Harald Weltec91085e2022-02-10 18:05:45 +0100225 def update_prompt(self):
226 if self.rs:
227 path_list = self.rs.selected_file.fully_qualified_path(
228 not self.numeric_path)
229 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
230 else:
Philipp Maier7226c092022-06-01 17:58:38 +0200231 if self.card:
232 self.prompt = 'pySIM-shell (no card profile)> '
233 else:
234 self.prompt = 'pySIM-shell (no card)> '
Harald Welteb2edd142021-01-08 23:29:35 +0100235
Harald Weltec91085e2022-02-10 18:05:45 +0100236 @cmd2.with_category(CUSTOM_CATEGORY)
237 def do_intro(self, _):
238 """Display the intro banner"""
239 self.poutput(self.intro)
Philipp Maier9764de22021-11-03 10:44:39 +0100240
Harald Weltec91085e2022-02-10 18:05:45 +0100241 def do_eof(self, _: argparse.Namespace) -> bool:
242 self.poutput("")
243 return self.do_quit('')
Philipp Maier5d698e52021-09-16 13:18:01 +0200244
Harald Weltec91085e2022-02-10 18:05:45 +0100245 @cmd2.with_category(CUSTOM_CATEGORY)
246 def do_equip(self, opts):
247 """Equip pySim-shell with card"""
248 rs, card = init_card(sl)
249 self.equip(card, rs)
Philipp Maier76667642021-09-22 16:53:22 +0200250
Philipp Maier7226c092022-06-01 17:58:38 +0200251 apdu_cmd_parser = argparse.ArgumentParser()
252 apdu_cmd_parser.add_argument('APDU', type=str, help='APDU as hex string')
Philipp Maiere7d1b672022-06-01 18:05:34 +0200253 apdu_cmd_parser.add_argument('--expect-sw', help='expect a specified status word', type=str, default=None)
Philipp Maier7226c092022-06-01 17:58:38 +0200254
255 @cmd2.with_argparser(apdu_cmd_parser)
256 def do_apdu(self, opts):
257 """Send a raw APDU to the card, and print SW + Response.
258 DANGEROUS: pySim-shell will not know any card state changes, and
259 not continue to work as expected if you e.g. select a different
260 file."""
261 data, sw = self.card._scc._tp.send_apdu(opts.APDU)
262 if data:
263 self.poutput("SW: %s, RESP: %s" % (sw, data))
264 else:
265 self.poutput("SW: %s" % sw)
Philipp Maiere7d1b672022-06-01 18:05:34 +0200266 if opts.expect_sw:
267 if not sw_match(sw, opts.expect_sw):
268 raise SwMatchError(sw, opts.expect_sw)
Philipp Maier7226c092022-06-01 17:58:38 +0200269
Harald Weltec91085e2022-02-10 18:05:45 +0100270 class InterceptStderr(list):
271 def __init__(self):
272 self._stderr_backup = sys.stderr
Philipp Maier76667642021-09-22 16:53:22 +0200273
Harald Weltec91085e2022-02-10 18:05:45 +0100274 def __enter__(self):
275 self._stringio_stderr = StringIO()
276 sys.stderr = self._stringio_stderr
277 return self
Philipp Maier76667642021-09-22 16:53:22 +0200278
Harald Weltec91085e2022-02-10 18:05:45 +0100279 def __exit__(self, *args):
280 self.stderr = self._stringio_stderr.getvalue().strip()
281 del self._stringio_stderr
282 sys.stderr = self._stderr_backup
Philipp Maier76667642021-09-22 16:53:22 +0200283
Harald Weltec91085e2022-02-10 18:05:45 +0100284 def _show_failure_sign(self):
285 self.poutput(style(" +-------------+", fg=fg.bright_red))
286 self.poutput(style(" + ## ## +", fg=fg.bright_red))
287 self.poutput(style(" + ## ## +", fg=fg.bright_red))
288 self.poutput(style(" + ### +", fg=fg.bright_red))
289 self.poutput(style(" + ## ## +", fg=fg.bright_red))
290 self.poutput(style(" + ## ## +", fg=fg.bright_red))
291 self.poutput(style(" +-------------+", fg=fg.bright_red))
292 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200293
Harald Weltec91085e2022-02-10 18:05:45 +0100294 def _show_success_sign(self):
295 self.poutput(style(" +-------------+", fg=fg.bright_green))
296 self.poutput(style(" + ## +", fg=fg.bright_green))
297 self.poutput(style(" + ## +", fg=fg.bright_green))
298 self.poutput(style(" + # ## +", fg=fg.bright_green))
299 self.poutput(style(" + ## # +", fg=fg.bright_green))
300 self.poutput(style(" + ## +", fg=fg.bright_green))
301 self.poutput(style(" +-------------+", fg=fg.bright_green))
302 self.poutput("")
Philipp Maier76667642021-09-22 16:53:22 +0200303
Harald Weltec91085e2022-02-10 18:05:45 +0100304 def _process_card(self, first, script_path):
Philipp Maier76667642021-09-22 16:53:22 +0200305
Harald Weltec91085e2022-02-10 18:05:45 +0100306 # Early phase of card initialzation (this part may fail with an exception)
307 try:
308 rs, card = init_card(self.sl)
309 rc = self.equip(card, rs)
310 except:
311 self.poutput("")
312 self.poutput("Card initialization failed with an exception:")
313 self.poutput("---------------------8<---------------------")
314 traceback.print_exc()
315 self.poutput("---------------------8<---------------------")
316 self.poutput("")
317 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200318
Harald Weltec91085e2022-02-10 18:05:45 +0100319 # Actual card processing step. This part should never fail with an exception since the cmd2
320 # do_run_script method will catch any exception that might occur during script execution.
321 if rc:
322 self.poutput("")
323 self.poutput("Transcript stdout:")
324 self.poutput("---------------------8<---------------------")
325 with self.InterceptStderr() as logged:
326 self.do_run_script(script_path)
327 self.poutput("---------------------8<---------------------")
Philipp Maier76667642021-09-22 16:53:22 +0200328
Harald Weltec91085e2022-02-10 18:05:45 +0100329 self.poutput("")
330 self.poutput("Transcript stderr:")
331 if logged.stderr:
332 self.poutput("---------------------8<---------------------")
333 self.poutput(logged.stderr)
334 self.poutput("---------------------8<---------------------")
335 else:
336 self.poutput("(none)")
Philipp Maier76667642021-09-22 16:53:22 +0200337
Harald Weltec91085e2022-02-10 18:05:45 +0100338 # Check for exceptions
339 self.poutput("")
340 if "EXCEPTION of type" not in logged.stderr:
341 return 0
Philipp Maier76667642021-09-22 16:53:22 +0200342
Harald Weltec91085e2022-02-10 18:05:45 +0100343 return -1
Philipp Maier76667642021-09-22 16:53:22 +0200344
Harald Weltec91085e2022-02-10 18:05:45 +0100345 bulk_script_parser = argparse.ArgumentParser()
346 bulk_script_parser.add_argument(
347 'script_path', help="path to the script file")
348 bulk_script_parser.add_argument('--halt_on_error', help='stop card handling if an exeption occurs',
349 action='store_true')
350 bulk_script_parser.add_argument('--tries', type=int, default=2,
351 help='how many tries before trying the next card')
352 bulk_script_parser.add_argument('--on_stop_action', type=str, default=None,
353 help='commandline to execute when card handling has stopped')
354 bulk_script_parser.add_argument('--pre_card_action', type=str, default=None,
355 help='commandline to execute before actually talking to the card')
Philipp Maier76667642021-09-22 16:53:22 +0200356
Harald Weltec91085e2022-02-10 18:05:45 +0100357 @cmd2.with_argparser(bulk_script_parser)
358 @cmd2.with_category(CUSTOM_CATEGORY)
359 def do_bulk_script(self, opts):
360 """Run script on multiple cards (bulk provisioning)"""
Philipp Maier76667642021-09-22 16:53:22 +0200361
Harald Weltec91085e2022-02-10 18:05:45 +0100362 # Make sure that the script file exists and that it is readable.
363 if not os.access(opts.script_path, os.R_OK):
364 self.poutput("Invalid script file!")
365 return
Philipp Maier76667642021-09-22 16:53:22 +0200366
Harald Weltec91085e2022-02-10 18:05:45 +0100367 success_count = 0
368 fail_count = 0
Philipp Maier76667642021-09-22 16:53:22 +0200369
Harald Weltec91085e2022-02-10 18:05:45 +0100370 first = True
371 while 1:
372 # TODO: Count consecutive failures, if more than N consecutive failures occur, then stop.
373 # The ratinale is: There may be a problem with the device, we do want to prevent that
374 # all remaining cards are fired to the error bin. This is only relevant for situations
375 # with large stacks, probably we do not need this feature right now.
Philipp Maier76667642021-09-22 16:53:22 +0200376
Harald Weltec91085e2022-02-10 18:05:45 +0100377 try:
378 # In case of failure, try multiple times.
379 for i in range(opts.tries):
380 # fetch card into reader bay
381 ch.get(first)
Philipp Maier76667642021-09-22 16:53:22 +0200382
Harald Weltec91085e2022-02-10 18:05:45 +0100383 # if necessary execute an action before we start processing the card
384 if(opts.pre_card_action):
385 os.system(opts.pre_card_action)
Philipp Maier76667642021-09-22 16:53:22 +0200386
Harald Weltec91085e2022-02-10 18:05:45 +0100387 # process the card
388 rc = self._process_card(first, opts.script_path)
389 if rc == 0:
390 success_count = success_count + 1
391 self._show_success_sign()
392 self.poutput("Statistics: success :%i, failure: %i" % (
393 success_count, fail_count))
394 break
395 else:
396 fail_count = fail_count + 1
397 self._show_failure_sign()
398 self.poutput("Statistics: success :%i, failure: %i" % (
399 success_count, fail_count))
Philipp Maier76667642021-09-22 16:53:22 +0200400
Harald Weltec91085e2022-02-10 18:05:45 +0100401 # Depending on success or failure, the card goes either in the "error" bin or in the
402 # "done" bin.
403 if rc < 0:
404 ch.error()
405 else:
406 ch.done()
Philipp Maier76667642021-09-22 16:53:22 +0200407
Harald Weltec91085e2022-02-10 18:05:45 +0100408 # In most cases it is possible to proceed with the next card, but the
409 # user may decide to halt immediately when an error occurs
410 if opts.halt_on_error and rc < 0:
411 return
Philipp Maier76667642021-09-22 16:53:22 +0200412
Harald Weltec91085e2022-02-10 18:05:45 +0100413 except (KeyboardInterrupt):
414 self.poutput("")
415 self.poutput("Terminated by user!")
416 return
417 except (SystemExit):
418 # When all cards are processed the card handler device will throw a SystemExit
419 # exception. Also Errors that are not recoverable (cards stuck etc.) will end up here.
420 # The user has the option to execute some action to make aware that the card handler
421 # needs service.
422 if(opts.on_stop_action):
423 os.system(opts.on_stop_action)
424 return
425 except:
426 self.poutput("")
427 self.poutput("Card handling failed with an exception:")
428 self.poutput("---------------------8<---------------------")
429 traceback.print_exc()
430 self.poutput("---------------------8<---------------------")
431 self.poutput("")
432 fail_count = fail_count + 1
433 self._show_failure_sign()
434 self.poutput("Statistics: success :%i, failure: %i" %
435 (success_count, fail_count))
Philipp Maierb52feed2021-09-22 16:43:13 +0200436
Harald Weltec91085e2022-02-10 18:05:45 +0100437 first = False
438
439 echo_parser = argparse.ArgumentParser()
440 echo_parser.add_argument('string', help="string to echo on the shell")
441
442 @cmd2.with_argparser(echo_parser)
443 @cmd2.with_category(CUSTOM_CATEGORY)
444 def do_echo(self, opts):
445 """Echo (print) a string on the console"""
446 self.poutput(opts.string)
447
Harald Welteb2edd142021-01-08 23:29:35 +0100448
Harald Welte31d2cf02021-04-03 10:47:29 +0200449@with_default_category('pySim Commands')
450class PySimCommands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100451 def __init__(self):
452 super().__init__()
Harald Welteb2edd142021-01-08 23:29:35 +0100453
Harald Weltec91085e2022-02-10 18:05:45 +0100454 dir_parser = argparse.ArgumentParser()
455 dir_parser.add_argument(
456 '--fids', help='Show file identifiers', action='store_true')
457 dir_parser.add_argument(
458 '--names', help='Show file names', action='store_true')
459 dir_parser.add_argument(
460 '--apps', help='Show applications', action='store_true')
461 dir_parser.add_argument(
462 '--all', help='Show all selectable identifiers and names', action='store_true')
Philipp Maier5d3e2592021-02-22 17:22:16 +0100463
Harald Weltec91085e2022-02-10 18:05:45 +0100464 @cmd2.with_argparser(dir_parser)
465 def do_dir(self, opts):
466 """Show a listing of files available in currently selected DF or MF"""
467 if opts.all:
468 flags = []
469 elif opts.fids or opts.names or opts.apps:
470 flags = ['PARENT', 'SELF']
471 if opts.fids:
472 flags += ['FIDS', 'AIDS']
473 if opts.names:
474 flags += ['FNAMES', 'ANAMES']
475 if opts.apps:
476 flags += ['ANAMES', 'AIDS']
477 else:
478 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
479 selectables = list(
480 self._cmd.rs.selected_file.get_selectable_names(flags=flags))
481 directory_str = tabulate_str_list(
482 selectables, width=79, hspace=2, lspace=1, align_left=True)
483 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
484 self._cmd.poutput('/'.join(path_list))
485 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
486 self._cmd.poutput('/'.join(path_list))
487 self._cmd.poutput(directory_str)
488 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100489
Philipp Maier7b138b02022-05-31 13:42:56 +0200490 def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
Harald Weltec91085e2022-02-10 18:05:45 +0100491 """Recursively walk through the file system, starting at the currently selected DF"""
Philipp Maier7b138b02022-05-31 13:42:56 +0200492
493 if isinstance(self._cmd.rs.selected_file, CardDF):
494 if action_df:
495 action_df(context, opts)
496
Harald Weltec91085e2022-02-10 18:05:45 +0100497 files = self._cmd.rs.selected_file.get_selectables(
498 flags=['FNAMES', 'ANAMES'])
499 for f in files:
Philipp Maier7b138b02022-05-31 13:42:56 +0200500 # special case: When no action is performed, just output a directory
501 if not action_ef and not action_df:
Harald Weltec91085e2022-02-10 18:05:45 +0100502 output_str = " " * indent + str(f) + (" " * 250)
503 output_str = output_str[0:25]
504 if isinstance(files[f], CardADF):
505 output_str += " " + str(files[f].aid)
506 else:
507 output_str += " " + str(files[f].fid)
508 output_str += " " + str(files[f].desc)
509 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200510
Harald Weltec91085e2022-02-10 18:05:45 +0100511 if isinstance(files[f], CardDF):
512 skip_df = False
513 try:
514 fcp_dec = self._cmd.rs.select(f, self._cmd)
515 except Exception as e:
516 skip_df = True
517 df = self._cmd.rs.selected_file
518 df_path_list = df.fully_qualified_path(True)
519 df_skip_reason_str = '/'.join(df_path_list) + \
520 "/" + str(f) + ", " + str(e)
521 if context:
522 context['DF_SKIP'] += 1
523 context['DF_SKIP_REASON'].append(df_skip_reason_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200524
Harald Weltec91085e2022-02-10 18:05:45 +0100525 # If the DF was skipped, we never have entered the directory
526 # below, so we must not move up.
527 if skip_df == False:
Philipp Maier7b138b02022-05-31 13:42:56 +0200528 self.walk(indent + 1, action_ef, action_df, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100529 fcp_dec = self._cmd.rs.select("..", self._cmd)
Philipp Maierf408a402021-04-09 21:16:12 +0200530
Philipp Maier7b138b02022-05-31 13:42:56 +0200531 elif action_ef:
Harald Weltec91085e2022-02-10 18:05:45 +0100532 df_before_action = self._cmd.rs.selected_file
Philipp Maier7b138b02022-05-31 13:42:56 +0200533 action_ef(f, context, **kwargs)
Harald Weltec91085e2022-02-10 18:05:45 +0100534 # When walking through the file system tree the action must not
535 # always restore the currently selected file to the file that
536 # was selected before executing the action() callback.
537 if df_before_action != self._cmd.rs.selected_file:
538 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
539 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100540
Harald Weltec91085e2022-02-10 18:05:45 +0100541 def do_tree(self, opts):
542 """Display a filesystem-tree with all selectable files"""
543 self.walk()
Philipp Maierff9dae22021-02-25 17:03:21 +0100544
Philipp Maier7b138b02022-05-31 13:42:56 +0200545 def export_ef(self, filename, context, as_json):
546 """ Select and export a single elementary file (EF) """
Harald Weltec91085e2022-02-10 18:05:45 +0100547 context['COUNT'] += 1
548 df = self._cmd.rs.selected_file
Philipp Maierac34dcc2021-04-01 17:19:05 +0200549
Philipp Maierea81f752022-05-19 10:13:30 +0200550 # The currently selected file (not the file we are going to export)
551 # must always be an ADF or DF. From this starting point we select
552 # the EF we want to export. To maintain consistency we will then
553 # select the current DF again (see comment below).
Harald Weltec91085e2022-02-10 18:05:45 +0100554 if not isinstance(df, CardDF):
555 raise RuntimeError(
556 "currently selected file %s is not a DF or ADF" % str(df))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200557
Harald Weltec91085e2022-02-10 18:05:45 +0100558 df_path_list = df.fully_qualified_path(True)
559 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100560
Harald Weltec91085e2022-02-10 18:05:45 +0100561 file_str = '/'.join(df_path_list) + "/" + str(filename)
562 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100563
Harald Weltec91085e2022-02-10 18:05:45 +0100564 self._cmd.poutput("# directory: %s (%s)" %
565 ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
566 try:
567 fcp_dec = self._cmd.rs.select(filename, self._cmd)
568 self._cmd.poutput("# file: %s (%s)" % (
569 self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100570
Harald Welte747a9782022-02-13 17:52:28 +0100571 structure = self._cmd.rs.selected_file_structure()
Harald Weltec91085e2022-02-10 18:05:45 +0100572 self._cmd.poutput("# structure: %s" % str(structure))
Harald Welte2bb17f32022-02-15 15:41:55 +0100573 self._cmd.poutput("# RAW FCP Template: %s" % str(self._cmd.rs.selected_file_fcp_hex))
574 self._cmd.poutput("# Decoded FCP Template: %s" % str(self._cmd.rs.selected_file_fcp))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100575
Harald Weltec91085e2022-02-10 18:05:45 +0100576 for f in df_path_list:
577 self._cmd.poutput("select " + str(f))
578 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100579
Harald Weltec91085e2022-02-10 18:05:45 +0100580 if structure == 'transparent':
Harald Welte08b11ab2022-02-10 18:56:41 +0100581 if as_json:
582 result = self._cmd.rs.read_binary_dec()
583 self._cmd.poutput("update_binary_decoded '%s'" % json.dumps(result[0], cls=JsonEncoder))
584 else:
585 result = self._cmd.rs.read_binary()
586 self._cmd.poutput("update_binary " + str(result[0]))
Harald Weltec91085e2022-02-10 18:05:45 +0100587 elif structure == 'cyclic' or structure == 'linear_fixed':
588 # Use number of records specified in select response
Harald Welte747a9782022-02-13 17:52:28 +0100589 num_of_rec = self._cmd.rs.selected_file_num_of_rec()
590 if num_of_rec:
Harald Weltec91085e2022-02-10 18:05:45 +0100591 for r in range(1, num_of_rec + 1):
Harald Welte08b11ab2022-02-10 18:56:41 +0100592 if as_json:
593 result = self._cmd.rs.read_record_dec(r)
594 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
595 else:
596 result = self._cmd.rs.read_record(r)
597 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
598
Harald Weltec91085e2022-02-10 18:05:45 +0100599 # When the select response does not return the number of records, read until we hit the
600 # first record that cannot be read.
601 else:
602 r = 1
603 while True:
604 try:
Harald Welte08b11ab2022-02-10 18:56:41 +0100605 if as_json:
606 result = self._cmd.rs.read_record_dec(r)
607 self._cmd.poutput("update_record_decoded %d '%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
608 else:
609 result = self._cmd.rs.read_record(r)
610 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Weltec91085e2022-02-10 18:05:45 +0100611 except SwMatchError as e:
612 # We are past the last valid record - stop
613 if e.sw_actual == "9402":
614 break
615 # Some other problem occurred
616 else:
617 raise e
Harald Weltec91085e2022-02-10 18:05:45 +0100618 r = r + 1
619 elif structure == 'ber_tlv':
620 tags = self._cmd.rs.retrieve_tags()
621 for t in tags:
622 result = self._cmd.rs.retrieve_data(t)
623 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
624 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
625 else:
626 raise RuntimeError(
627 'Unsupported structure "%s" of file "%s"' % (structure, filename))
628 except Exception as e:
629 bad_file_str = '/'.join(df_path_list) + \
630 "/" + str(filename) + ", " + str(e)
631 self._cmd.poutput("# bad file: %s" % bad_file_str)
632 context['ERR'] += 1
633 context['BAD'].append(bad_file_str)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100634
Harald Weltec91085e2022-02-10 18:05:45 +0100635 # When reading the file is done, make sure the parent file is
636 # selected again. This will be the usual case, however we need
637 # to check before since we must not select the same DF twice
638 if df != self._cmd.rs.selected_file:
639 self._cmd.rs.select(df.fid or df.aid, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200640
Harald Weltec91085e2022-02-10 18:05:45 +0100641 self._cmd.poutput("#")
Philipp Maier24f7bd32021-02-25 17:06:18 +0100642
Harald Weltec91085e2022-02-10 18:05:45 +0100643 export_parser = argparse.ArgumentParser()
644 export_parser.add_argument(
645 '--filename', type=str, default=None, help='only export specific file')
Harald Welte08b11ab2022-02-10 18:56:41 +0100646 export_parser.add_argument(
647 '--json', action='store_true', help='export as JSON (less reliable)')
Philipp Maier24f7bd32021-02-25 17:06:18 +0100648
Harald Weltec91085e2022-02-10 18:05:45 +0100649 @cmd2.with_argparser(export_parser)
650 def do_export(self, opts):
651 """Export files to script that can be imported back later"""
652 context = {'ERR': 0, 'COUNT': 0, 'BAD': [],
653 'DF_SKIP': 0, 'DF_SKIP_REASON': []}
Philipp Maier9a4091d2022-05-19 10:20:30 +0200654 kwargs_export = {'as_json': opts.json}
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200655 exception_str_add = ""
656
Harald Weltec91085e2022-02-10 18:05:45 +0100657 if opts.filename:
Philipp Maier7b138b02022-05-31 13:42:56 +0200658 self.export_ef(opts.filename, context, **kwargs_export)
Harald Weltec91085e2022-02-10 18:05:45 +0100659 else:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200660 try:
661 self.walk(0, self.export_ef, None, context, **kwargs_export)
662 except Exception as e:
663 print("# Stopping early here due to exception: " + str(e))
664 print("#")
665 exception_str_add = ", also had to stop early due to exception:" + str(e)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200666
Harald Weltec91085e2022-02-10 18:05:45 +0100667 self._cmd.poutput(boxed_heading_str("Export summary"))
Philipp Maier80ce71f2021-04-19 21:24:23 +0200668
Harald Weltec91085e2022-02-10 18:05:45 +0100669 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
670 self._cmd.poutput("# bad files: %u" % context['ERR'])
671 for b in context['BAD']:
672 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200673
Harald Weltec91085e2022-02-10 18:05:45 +0100674 self._cmd.poutput("# skipped dedicated files(s): %u" %
675 context['DF_SKIP'])
676 for b in context['DF_SKIP_REASON']:
677 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200678
Harald Weltec91085e2022-02-10 18:05:45 +0100679 if context['ERR'] and context['DF_SKIP']:
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200680 raise RuntimeError("unable to export %i elementary file(s) and %i dedicated file(s)%s" % (
681 context['ERR'], context['DF_SKIP'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100682 elif context['ERR']:
683 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200684 "unable to export %i elementary file(s)%s" % (context['ERR'], exception_str_add))
Harald Weltec91085e2022-02-10 18:05:45 +0100685 elif context['DF_SKIP']:
686 raise RuntimeError(
Philipp Maierf16ac6a2022-05-31 14:08:47 +0200687 "unable to export %i dedicated files(s)%s" % (context['ERR'], exception_str_add))
Harald Welteb2edd142021-01-08 23:29:35 +0100688
Harald Weltec91085e2022-02-10 18:05:45 +0100689 def do_reset(self, opts):
690 """Reset the Card."""
691 atr = self._cmd.rs.reset(self._cmd)
692 self._cmd.poutput('Card ATR: %s' % atr)
693 self._cmd.update_prompt()
Harald Weltedaf2b392021-05-03 23:17:29 +0200694
Harald Weltec91085e2022-02-10 18:05:45 +0100695 def do_desc(self, opts):
696 """Display human readable file description for the currently selected file"""
697 desc = self._cmd.rs.selected_file.desc
698 if desc:
699 self._cmd.poutput(desc)
700 else:
701 self._cmd.poutput("no description available")
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200702
Harald Weltec91085e2022-02-10 18:05:45 +0100703 def do_verify_adm(self, arg):
704 """VERIFY the ADM1 PIN"""
705 if arg:
706 # use specified ADM-PIN
707 pin_adm = sanitize_pin_adm(arg)
708 else:
709 # try to find an ADM-PIN if none is specified
710 result = card_key_provider_get_field(
711 'ADM1', key='ICCID', value=self._cmd.iccid)
712 pin_adm = sanitize_pin_adm(result)
713 if pin_adm:
714 self._cmd.poutput(
715 "found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
716 else:
717 raise ValueError(
718 "cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200719
Harald Weltec91085e2022-02-10 18:05:45 +0100720 if pin_adm:
721 self._cmd.card.verify_adm(h2b(pin_adm))
722 else:
723 raise ValueError("error: cannot authenticate, no adm-pin!")
724
Harald Welteb2edd142021-01-08 23:29:35 +0100725
Harald Welte31d2cf02021-04-03 10:47:29 +0200726@with_default_category('ISO7816 Commands')
727class Iso7816Commands(CommandSet):
Harald Weltec91085e2022-02-10 18:05:45 +0100728 def __init__(self):
729 super().__init__()
Harald Welte31d2cf02021-04-03 10:47:29 +0200730
Harald Weltec91085e2022-02-10 18:05:45 +0100731 def do_select(self, opts):
732 """SELECT a File (ADF/DF/EF)"""
733 if len(opts.arg_list) == 0:
734 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
735 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(
736 False)
737 self._cmd.poutput("currently selected file: " +
738 '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
739 return
Harald Welte31d2cf02021-04-03 10:47:29 +0200740
Harald Weltec91085e2022-02-10 18:05:45 +0100741 path = opts.arg_list[0]
742 fcp_dec = self._cmd.rs.select(path, self._cmd)
743 self._cmd.update_prompt()
744 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200745
Harald Weltec91085e2022-02-10 18:05:45 +0100746 def complete_select(self, text, line, begidx, endidx) -> List[str]:
747 """Command Line tab completion for SELECT"""
748 index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
749 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200750
Harald Weltec91085e2022-02-10 18:05:45 +0100751 def get_code(self, code):
752 """Use code either directly or try to get it from external data source"""
753 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
Harald Welte31d2cf02021-04-03 10:47:29 +0200754
Harald Weltec91085e2022-02-10 18:05:45 +0100755 if str(code).upper() not in auto:
756 return sanitize_pin_adm(code)
Harald Welte31d2cf02021-04-03 10:47:29 +0200757
Harald Weltec91085e2022-02-10 18:05:45 +0100758 result = card_key_provider_get_field(
759 str(code), key='ICCID', value=self._cmd.iccid)
760 result = sanitize_pin_adm(result)
761 if result:
762 self._cmd.poutput("found %s '%s' for ICCID '%s'" %
763 (code.upper(), result, self._cmd.iccid))
764 else:
765 self._cmd.poutput("cannot find %s for ICCID '%s'" %
766 (code.upper(), self._cmd.iccid))
767 return result
Harald Welte31d2cf02021-04-03 10:47:29 +0200768
Harald Weltec91085e2022-02-10 18:05:45 +0100769 verify_chv_parser = argparse.ArgumentParser()
770 verify_chv_parser.add_argument(
771 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
772 verify_chv_parser.add_argument(
773 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200774
Harald Weltec91085e2022-02-10 18:05:45 +0100775 @cmd2.with_argparser(verify_chv_parser)
776 def do_verify_chv(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100777 """Verify (authenticate) using specified CHV (PIN) code, which is how the specifications
778 call it if you authenticate yourself using the specified PIN. There usually is at least PIN1 and
779 PIN2."""
Harald Weltec91085e2022-02-10 18:05:45 +0100780 pin = self.get_code(opts.pin_code)
781 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
782 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200783
Harald Weltec91085e2022-02-10 18:05:45 +0100784 unblock_chv_parser = argparse.ArgumentParser()
785 unblock_chv_parser.add_argument(
786 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
787 unblock_chv_parser.add_argument(
788 'puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
789 unblock_chv_parser.add_argument(
790 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200791
Harald Weltec91085e2022-02-10 18:05:45 +0100792 @cmd2.with_argparser(unblock_chv_parser)
793 def do_unblock_chv(self, opts):
794 """Unblock PIN code using specified PUK code"""
795 new_pin = self.get_code(opts.new_pin_code)
796 puk = self.get_code(opts.puk_code)
797 (data, sw) = self._cmd.card._scc.unblock_chv(
798 opts.pin_nr, h2b(puk), h2b(new_pin))
799 self._cmd.poutput("CHV unblock successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200800
Harald Weltec91085e2022-02-10 18:05:45 +0100801 change_chv_parser = argparse.ArgumentParser()
802 change_chv_parser.add_argument(
803 '--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
804 change_chv_parser.add_argument(
805 'pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
806 change_chv_parser.add_argument(
807 'new_pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200808
Harald Weltec91085e2022-02-10 18:05:45 +0100809 @cmd2.with_argparser(change_chv_parser)
810 def do_change_chv(self, opts):
811 """Change PIN code to a new PIN code"""
812 new_pin = self.get_code(opts.new_pin_code)
813 pin = self.get_code(opts.pin_code)
814 (data, sw) = self._cmd.card._scc.change_chv(
815 opts.pin_nr, h2b(pin), h2b(new_pin))
816 self._cmd.poutput("CHV change successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200817
Harald Weltec91085e2022-02-10 18:05:45 +0100818 disable_chv_parser = argparse.ArgumentParser()
819 disable_chv_parser.add_argument(
820 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
821 disable_chv_parser.add_argument(
822 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200823
Harald Weltec91085e2022-02-10 18:05:45 +0100824 @cmd2.with_argparser(disable_chv_parser)
825 def do_disable_chv(self, opts):
826 """Disable PIN code using specified PIN code"""
827 pin = self.get_code(opts.pin_code)
828 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
829 self._cmd.poutput("CHV disable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200830
Harald Weltec91085e2022-02-10 18:05:45 +0100831 enable_chv_parser = argparse.ArgumentParser()
832 enable_chv_parser.add_argument(
833 '--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
834 enable_chv_parser.add_argument(
835 'pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
Harald Welte31d2cf02021-04-03 10:47:29 +0200836
Harald Weltec91085e2022-02-10 18:05:45 +0100837 @cmd2.with_argparser(enable_chv_parser)
838 def do_enable_chv(self, opts):
839 """Enable PIN code using specified PIN code"""
840 pin = self.get_code(opts.pin_code)
841 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
842 self._cmd.poutput("CHV enable successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200843
Harald Weltec91085e2022-02-10 18:05:45 +0100844 def do_deactivate_file(self, opts):
Harald Welte799c3542022-02-15 15:56:28 +0100845 """Deactivate the currently selected EF"""
Harald Weltec91085e2022-02-10 18:05:45 +0100846 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200847
Harald Welte799c3542022-02-15 15:56:28 +0100848 activate_file_parser = argparse.ArgumentParser()
849 activate_file_parser.add_argument('NAME', type=str, help='File name or FID of file to activate')
850 @cmd2.with_argparser(activate_file_parser)
Harald Weltec91085e2022-02-10 18:05:45 +0100851 def do_activate_file(self, opts):
Harald Welte12af7932022-02-15 16:39:08 +0100852 """Activate the specified EF. This used to be called REHABILITATE in TS 11.11 for classic
853 SIM. You need to specify the name or FID of the file to activate."""
Harald Welte799c3542022-02-15 15:56:28 +0100854 (data, sw) = self._cmd.rs.activate_file(opts.NAME)
Harald Welte485692b2021-05-25 22:21:44 +0200855
Harald Weltec91085e2022-02-10 18:05:45 +0100856 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
857 """Command Line tab completion for ACTIVATE FILE"""
858 index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
859 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200860
Harald Weltec91085e2022-02-10 18:05:45 +0100861 open_chan_parser = argparse.ArgumentParser()
862 open_chan_parser.add_argument(
863 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200864
Harald Weltec91085e2022-02-10 18:05:45 +0100865 @cmd2.with_argparser(open_chan_parser)
866 def do_open_channel(self, opts):
867 """Open a logical channel."""
868 (data, sw) = self._cmd.card._scc.manage_channel(
869 mode='open', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200870
Harald Weltec91085e2022-02-10 18:05:45 +0100871 close_chan_parser = argparse.ArgumentParser()
872 close_chan_parser.add_argument(
873 'chan_nr', type=int, default=0, help='Channel Number')
Harald Welte703f9332021-04-10 18:39:32 +0200874
Harald Weltec91085e2022-02-10 18:05:45 +0100875 @cmd2.with_argparser(close_chan_parser)
876 def do_close_channel(self, opts):
877 """Close a logical channel."""
878 (data, sw) = self._cmd.card._scc.manage_channel(
879 mode='close', lchan_nr=opts.chan_nr)
Harald Welte703f9332021-04-10 18:39:32 +0200880
Harald Weltec91085e2022-02-10 18:05:45 +0100881 def do_status(self, opts):
882 """Perform the STATUS command."""
883 fcp_dec = self._cmd.rs.status()
884 self._cmd.poutput_json(fcp_dec)
Harald Welte34b05d32021-05-25 22:03:13 +0200885
Harald Weltec91085e2022-02-10 18:05:45 +0100886 suspend_uicc_parser = argparse.ArgumentParser()
887 suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
888 help='Proposed minimum duration of suspension')
889 suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
890 help='Proposed maximum duration of suspension')
Harald Welteec950532021-10-20 13:09:00 +0200891
Harald Weltec91085e2022-02-10 18:05:45 +0100892 # not ISO7816-4 but TS 102 221
893 @cmd2.with_argparser(suspend_uicc_parser)
894 def do_suspend_uicc(self, opts):
895 """Perform the SUSPEND UICC command. Only supported on some UICC."""
896 (duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
897 max_len_secs=opts.max_duration_secs)
898 self._cmd.poutput(
899 'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
Harald Welteec950532021-10-20 13:09:00 +0200900
Harald Welte703f9332021-04-10 18:39:32 +0200901
Harald Weltef2e761c2021-04-11 11:56:44 +0200902option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
903 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200904argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200905
906global_group = option_parser.add_argument_group('General Options')
907global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200908 help='script with pySim-shell commands to be executed automatically at start-up')
Harald Weltec91085e2022-02-10 18:05:45 +0100909global_group.add_argument('--csv', metavar='FILE',
910 default=None, help='Read card data from CSV file')
Philipp Maier76667642021-09-22 16:53:22 +0200911global_group.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
Harald Weltec91085e2022-02-10 18:05:45 +0100912 help="Use automatic card handling machine")
Harald Weltec8ff0262021-04-11 12:06:13 +0200913
914adm_group = global_group.add_mutually_exclusive_group()
915adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
916 help='ADM PIN used for provisioning (overwrites default)')
917adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
918 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100919
920
921if __name__ == '__main__':
922
Harald Weltec91085e2022-02-10 18:05:45 +0100923 # Parse options
924 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100925
Harald Weltec91085e2022-02-10 18:05:45 +0100926 # If a script file is specified, be sure that it actually exists
927 if opts.script:
928 if not os.access(opts.script, os.R_OK):
929 print("Invalid script file!")
930 sys.exit(2)
Philipp Maier13e258d2021-04-08 17:48:49 +0200931
Harald Weltec91085e2022-02-10 18:05:45 +0100932 # Register csv-file as card data provider, either from specified CSV
933 # or from CSV file in home directory
934 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
935 if opts.csv:
936 card_key_provider_register(CardKeyProviderCsv(opts.csv))
937 if os.path.isfile(csv_default):
938 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100939
Harald Weltec91085e2022-02-10 18:05:45 +0100940 # Init card reader driver
941 sl = init_reader(opts)
942 if sl is None:
943 exit(1)
Philipp Maierea95c392021-09-16 13:10:19 +0200944
Harald Weltec91085e2022-02-10 18:05:45 +0100945 # Create command layer
946 scc = SimCardCommands(transport=sl)
Philipp Maierea95c392021-09-16 13:10:19 +0200947
Harald Weltec91085e2022-02-10 18:05:45 +0100948 # Create a card handler (for bulk provisioning)
949 if opts.card_handler_config:
950 ch = CardHandlerAuto(None, opts.card_handler_config)
951 else:
952 ch = CardHandler(sl)
Philipp Maier76667642021-09-22 16:53:22 +0200953
Harald Weltec91085e2022-02-10 18:05:45 +0100954 # Detect and initialize the card in the reader. This may fail when there
955 # is no card in the reader or the card is unresponsive. PysimApp is
956 # able to tolerate and recover from that.
957 try:
958 rs, card = init_card(sl)
959 app = PysimApp(card, rs, sl, ch, opts.script)
960 except:
961 print("Card initialization failed with an exception:")
962 print("---------------------8<---------------------")
963 traceback.print_exc()
964 print("---------------------8<---------------------")
965 print("(you may still try to recover from this manually by using the 'equip' command.)")
966 print(
967 " it should also be noted that some readers may behave strangely when no card")
968 print(" is inserted.)")
969 print("")
Philipp Maier7226c092022-06-01 17:58:38 +0200970 app = PysimApp(card, None, sl, ch, opts.script)
Philipp Maierea95c392021-09-16 13:10:19 +0200971
Harald Weltec91085e2022-02-10 18:05:45 +0100972 # If the user supplies an ADM PIN at via commandline args authenticate
973 # immediately so that the user does not have to use the shell commands
974 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
975 if pin_adm:
976 if not card:
977 print("Card error, cannot do ADM verification with supplied ADM pin now.")
978 try:
979 card.verify_adm(h2b(pin_adm))
980 except Exception as e:
981 print(e)
Philipp Maier228c98e2021-03-10 20:14:06 +0100982
Harald Weltec91085e2022-02-10 18:05:45 +0100983 app.cmdloop()