blob: 871c45ece67a3290ee5e1995aa8b54920b9922dd [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
23
24import cmd2
25from cmd2 import style, fg, bg
26from cmd2 import CommandSet, with_default_category, with_argparser
27import argparse
28
29import os
30import sys
Philipp Maier2b11c322021-03-17 12:37:39 +010031from pathlib import Path
Harald Welteb2edd142021-01-08 23:29:35 +010032
Robert Falkenberg9d16fbc2021-04-12 11:43:22 +020033from pySim.ts_51_011 import EF, DF, EF_SST_map
Harald Welteb2edd142021-01-08 23:29:35 +010034from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
35from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
36
37from pySim.exceptions import *
38from pySim.commands import SimCardCommands
Harald Welte28c24312021-04-11 12:19:36 +020039from pySim.transport import init_reader, ApduTracer, argparse_add_reader_args
Philipp Maierbb73e512021-05-05 16:14:00 +020040from pySim.cards import card_detect, SimCard
Harald Welte917d98c2021-04-21 11:51:25 +020041from pySim.utils import h2b, swap_nibbles, rpad, b2h, h2s, JsonEncoder, bertlv_parse_one
Philipp Maier80ce71f2021-04-19 21:24:23 +020042from pySim.utils import dec_st, sanitize_pin_adm, tabulate_str_list, is_hex, boxed_heading_str
Philipp Maierb18eed02021-09-17 12:35:58 +020043from pySim.card_handler import CardHandler
Harald Welteb2edd142021-01-08 23:29:35 +010044
Harald Weltef44256c2021-10-14 15:53:39 +020045from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF, CardModel
Harald Welteb2edd142021-01-08 23:29:35 +010046from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
47from pySim.ts_102_221 import CardProfileUICC
Harald Welte5ce35242021-04-02 20:27:05 +020048from pySim.ts_31_102 import CardApplicationUSIM
49from pySim.ts_31_103 import CardApplicationISIM
Harald Welteb2edd142021-01-08 23:29:35 +010050
Harald Weltef44256c2021-10-14 15:53:39 +020051import pySim.sysmocom_sja2
52
Harald Welte4442b3d2021-04-03 09:00:16 +020053from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010054
Philipp Maierea95c392021-09-16 13:10:19 +020055def init_card(sl):
56 """
57 Detect card in reader and setup card profile and runtime state. This
58 function must be called at least once on startup. The card and runtime
59 state object (rs) is required for all pySim-shell commands.
60 """
61
62 # Wait up to three seconds for a card in reader and try to detect
63 # the card type.
64 print("Waiting for card...")
65 try:
66 sl.wait_for_card(3)
67 except NoCardError:
68 print("No card detected!")
69 return None, None;
70 except:
71 print("Card not readable!")
72 return None, None;
73
74 card = card_detect("auto", scc)
75 if card is None:
76 print("Could not detect card type!")
77 return None, None;
78
79 # Create runtime state with card profile
80 profile = CardProfileUICC()
81 profile.add_application(CardApplicationUSIM)
82 profile.add_application(CardApplicationISIM)
83 rs = RuntimeState(card, profile)
84
85 # FIXME: do this dynamically
86 rs.mf.add_file(DF_TELECOM())
87 rs.mf.add_file(DF_GSM())
88
Harald Weltef44256c2021-10-14 15:53:39 +020089 CardModel.apply_matching_models(scc, rs)
90
Philipp Maierea95c392021-09-16 13:10:19 +020091 # inform the transport that we can do context-specific SW interpretation
92 sl.set_sw_interpreter(rs)
93
94 return rs, card
Philipp Maier2b11c322021-03-17 12:37:39 +010095
Harald Welteb2edd142021-01-08 23:29:35 +010096class PysimApp(cmd2.Cmd):
97 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier681bc7b2021-03-10 19:52:41 +010098 def __init__(self, card, rs, script = None):
Harald Welte31d2cf02021-04-03 10:47:29 +020099 basic_commands = [Iso7816Commands(), PySimCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +0100100 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier681bc7b2021-03-10 19:52:41 +0100101 use_ipython=True, auto_load_commands=False, command_sets=basic_commands, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +0100102 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
103 self.default_category = 'pySim-shell built-in commands'
104 self.card = card
Philipp Maier2b11c322021-03-17 12:37:39 +0100105 iccid, sw = self.card.read_iccid()
106 self.iccid = iccid
Harald Welteb2edd142021-01-08 23:29:35 +0100107 self.rs = rs
108 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Harald Welteb2edd142021-01-08 23:29:35 +0100109 self.numeric_path = False
110 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
111 onchange_cb=self._onchange_numeric_path))
Philipp Maier38c74f62021-03-17 17:19:52 +0100112 self.conserve_write = True
113 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
114 onchange_cb=self._onchange_conserve_write))
Harald Welteb2edd142021-01-08 23:29:35 +0100115 self.update_prompt()
Harald Welte1748b932021-04-06 21:12:25 +0200116 self.json_pretty_print = True
117 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Harald Welte7829d8a2021-04-10 11:28:53 +0200118 self.apdu_trace = False
119 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
120 onchange_cb=self._onchange_apdu_trace))
Harald Welte1748b932021-04-06 21:12:25 +0200121
122 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +0200123 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +0200124 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +0200125 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +0200126 else:
Harald Welte5e749a72021-04-10 17:18:17 +0200127 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +0200128 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100129
130 def _onchange_numeric_path(self, param_name, old, new):
131 self.update_prompt()
132
Philipp Maier38c74f62021-03-17 17:19:52 +0100133 def _onchange_conserve_write(self, param_name, old, new):
134 self.rs.conserve_write = new
135
Harald Welte7829d8a2021-04-10 11:28:53 +0200136 def _onchange_apdu_trace(self, param_name, old, new):
137 if new == True:
138 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
139 else:
140 self.card._scc._tp.apdu_tracer = None
141
142 class Cmd2ApduTracer(ApduTracer):
143 def __init__(self, cmd2_app):
144 self.cmd2 = app
145
146 def trace_response(self, cmd, sw, resp):
147 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
148 self.cmd2.poutput("<- %s: %s" % (sw, resp))
149
Harald Welteb2edd142021-01-08 23:29:35 +0100150 def update_prompt(self):
151 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
152 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
153
154 @cmd2.with_category(CUSTOM_CATEGORY)
155 def do_intro(self, _):
156 """Display the intro banner"""
157 self.poutput(self.intro)
158
Harald Welteb2edd142021-01-08 23:29:35 +0100159
Harald Welte31d2cf02021-04-03 10:47:29 +0200160@with_default_category('pySim Commands')
161class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100162 def __init__(self):
163 super().__init__()
164
Philipp Maier5d3e2592021-02-22 17:22:16 +0100165 dir_parser = argparse.ArgumentParser()
166 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
167 dir_parser.add_argument('--names', help='Show file names', action='store_true')
168 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
169 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
170
171 @cmd2.with_argparser(dir_parser)
172 def do_dir(self, opts):
173 """Show a listing of files available in currently selected DF or MF"""
174 if opts.all:
175 flags = []
176 elif opts.fids or opts.names or opts.apps:
177 flags = ['PARENT', 'SELF']
178 if opts.fids:
179 flags += ['FIDS', 'AIDS']
180 if opts.names:
181 flags += ['FNAMES', 'ANAMES']
182 if opts.apps:
183 flags += ['ANAMES', 'AIDS']
184 else:
185 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
186 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
187 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
188 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
189 self._cmd.poutput('/'.join(path_list))
190 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
191 self._cmd.poutput('/'.join(path_list))
192 self._cmd.poutput(directory_str)
193 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100194
Philipp Maierff9dae22021-02-25 17:03:21 +0100195 def walk(self, indent = 0, action = None, context = None):
196 """Recursively walk through the file system, starting at the currently selected DF"""
197 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
198 for f in files:
199 if not action:
200 output_str = " " * indent + str(f) + (" " * 250)
201 output_str = output_str[0:25]
202 if isinstance(files[f], CardADF):
203 output_str += " " + str(files[f].aid)
204 else:
205 output_str += " " + str(files[f].fid)
206 output_str += " " + str(files[f].desc)
207 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200208
Philipp Maierff9dae22021-02-25 17:03:21 +0100209 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200210 skip_df=False
211 try:
212 fcp_dec = self._cmd.rs.select(f, self._cmd)
213 except Exception as e:
214 skip_df=True
215 df = self._cmd.rs.selected_file
216 df_path_list = df.fully_qualified_path(True)
217 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
218 if context:
219 context['DF_SKIP'] += 1
220 context['DF_SKIP_REASON'].append(df_skip_reason_str)
221
222 # If the DF was skipped, we never have entered the directory
223 # below, so we must not move up.
224 if skip_df == False:
225 self.walk(indent + 1, action, context)
226 fcp_dec = self._cmd.rs.select("..", self._cmd)
227
Philipp Maierff9dae22021-02-25 17:03:21 +0100228 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200229 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100230 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200231 # When walking through the file system tree the action must not
232 # always restore the currently selected file to the file that
233 # was selected before executing the action() callback.
234 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200235 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200236 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100237
238 def do_tree(self, opts):
239 """Display a filesystem-tree with all selectable files"""
240 self.walk()
241
Philipp Maier24f7bd32021-02-25 17:06:18 +0100242 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200243 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100244 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200245 df = self._cmd.rs.selected_file
246
247 if not isinstance(df, CardDF):
248 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
249
250 df_path_list = df.fully_qualified_path(True)
251 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100252
Philipp Maier80ce71f2021-04-19 21:24:23 +0200253 file_str = '/'.join(df_path_list) + "/" + str(filename)
254 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100255
Philipp Maierac34dcc2021-04-01 17:19:05 +0200256 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100257 try:
258 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200259 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 +0100260
261 fd = fcp_dec['file_descriptor']
262 structure = fd['structure']
263 self._cmd.poutput("# structure: %s" % str(structure))
264
Philipp Maierac34dcc2021-04-01 17:19:05 +0200265 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100266 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200267 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100268
269 if structure == 'transparent':
270 result = self._cmd.rs.read_binary()
271 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200272 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100273 num_of_rec = fd['num_of_rec']
274 for r in range(1, num_of_rec + 1):
275 result = self._cmd.rs.read_record(r)
276 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200277 elif structure == 'ber_tlv':
278 tags = self._cmd.rs.retrieve_tags()
279 for t in tags:
280 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200281 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200282 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
283 else:
284 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100285 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200286 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100287 self._cmd.poutput("# bad file: %s" % bad_file_str)
288 context['ERR'] += 1
289 context['BAD'].append(bad_file_str)
290
Philipp Maierac34dcc2021-04-01 17:19:05 +0200291 # When reading the file is done, make sure the parent file is
292 # selected again. This will be the usual case, however we need
293 # to check before since we must not select the same DF twice
294 if df != self._cmd.rs.selected_file:
295 self._cmd.rs.select(df.fid or df.aid, self._cmd)
296
Philipp Maier24f7bd32021-02-25 17:06:18 +0100297 self._cmd.poutput("#")
298
299 export_parser = argparse.ArgumentParser()
300 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
301
302 @cmd2.with_argparser(export_parser)
303 def do_export(self, opts):
304 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200305 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100306 if opts.filename:
307 self.export(opts.filename, context)
308 else:
309 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200310
311 self._cmd.poutput(boxed_heading_str("Export summary"))
312
Philipp Maier24f7bd32021-02-25 17:06:18 +0100313 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
314 self._cmd.poutput("# bad files: %u" % context['ERR'])
315 for b in context['BAD']:
316 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200317
318 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
319 for b in context['DF_SKIP_REASON']:
320 self._cmd.poutput("# " + b)
321
322 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200323 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 +0200324 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200325 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200326 elif context['DF_SKIP']:
327 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100328
Harald Weltedaf2b392021-05-03 23:17:29 +0200329 def do_reset(self, opts):
330 """Reset the Card."""
331 atr = self._cmd.rs.reset(self._cmd)
332 self._cmd.poutput('Card ATR: %s' % atr)
333 self._cmd.update_prompt()
334
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200335 def do_desc(self, opts):
336 """Display human readable file description for the currently selected file"""
337 desc = self._cmd.rs.selected_file.desc
338 if desc:
339 self._cmd.poutput(desc)
340 else:
341 self._cmd.poutput("no description available")
342
343 def do_verify_adm(self, arg):
344 """VERIFY the ADM1 PIN"""
345 if arg:
346 # use specified ADM-PIN
347 pin_adm = sanitize_pin_adm(arg)
348 else:
349 # try to find an ADM-PIN if none is specified
350 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
351 pin_adm = sanitize_pin_adm(result)
352 if pin_adm:
353 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
354 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200355 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200356
357 if pin_adm:
358 self._cmd.card.verify_adm(h2b(pin_adm))
359 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200360 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100361
Harald Welte31d2cf02021-04-03 10:47:29 +0200362@with_default_category('ISO7816 Commands')
363class Iso7816Commands(CommandSet):
364 def __init__(self):
365 super().__init__()
366
367 def do_select(self, opts):
368 """SELECT a File (ADF/DF/EF)"""
369 if len(opts.arg_list) == 0:
370 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
371 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
372 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
373 return
374
375 path = opts.arg_list[0]
376 fcp_dec = self._cmd.rs.select(path, self._cmd)
377 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200378 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200379
380 def complete_select(self, text, line, begidx, endidx) -> List[str]:
381 """Command Line tab completion for SELECT"""
382 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
383 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
384
385 def get_code(self, code):
386 """Use code either directly or try to get it from external data source"""
387 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
388
389 if str(code).upper() not in auto:
390 return sanitize_pin_adm(code)
391
392 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
393 result = sanitize_pin_adm(result)
394 if result:
395 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
396 else:
397 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
398 return result
399
400 verify_chv_parser = argparse.ArgumentParser()
401 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
402 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
403
404 @cmd2.with_argparser(verify_chv_parser)
405 def do_verify_chv(self, opts):
406 """Verify (authenticate) using specified PIN code"""
407 pin = self.get_code(opts.pin_code)
408 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200409 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200410
411 unblock_chv_parser = argparse.ArgumentParser()
412 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
413 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
414 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')
415
416 @cmd2.with_argparser(unblock_chv_parser)
417 def do_unblock_chv(self, opts):
418 """Unblock PIN code using specified PUK code"""
419 new_pin = self.get_code(opts.new_pin_code)
420 puk = self.get_code(opts.puk_code)
421 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
422 self._cmd.poutput("CHV unblock successful")
423
424 change_chv_parser = argparse.ArgumentParser()
425 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
426 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
427 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')
428
429 @cmd2.with_argparser(change_chv_parser)
430 def do_change_chv(self, opts):
431 """Change PIN code to a new PIN code"""
432 new_pin = self.get_code(opts.new_pin_code)
433 pin = self.get_code(opts.pin_code)
434 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
435 self._cmd.poutput("CHV change successful")
436
437 disable_chv_parser = argparse.ArgumentParser()
438 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
439 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
440
441 @cmd2.with_argparser(disable_chv_parser)
442 def do_disable_chv(self, opts):
443 """Disable PIN code using specified PIN code"""
444 pin = self.get_code(opts.pin_code)
445 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
446 self._cmd.poutput("CHV disable successful")
447
448 enable_chv_parser = argparse.ArgumentParser()
449 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
450 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
451
452 @cmd2.with_argparser(enable_chv_parser)
453 def do_enable_chv(self, opts):
454 """Enable PIN code using specified PIN code"""
455 pin = self.get_code(opts.pin_code)
456 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
457 self._cmd.poutput("CHV enable successful")
458
Harald Weltea4631612021-04-10 18:17:55 +0200459 def do_deactivate_file(self, opts):
460 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200461 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200462
463 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200464 """Activate the specified EF"""
465 path = opts.arg_list[0]
466 (data, sw) = self._cmd.rs.activate_file(path)
467
468 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
469 """Command Line tab completion for ACTIVATE FILE"""
470 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
471 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200472
Harald Welte703f9332021-04-10 18:39:32 +0200473 open_chan_parser = argparse.ArgumentParser()
474 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
475
476 @cmd2.with_argparser(open_chan_parser)
477 def do_open_channel(self, opts):
478 """Open a logical channel."""
479 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
480
481 close_chan_parser = argparse.ArgumentParser()
482 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
483
484 @cmd2.with_argparser(close_chan_parser)
485 def do_close_channel(self, opts):
486 """Close a logical channel."""
487 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
488
Harald Welte34b05d32021-05-25 22:03:13 +0200489 def do_status(self, opts):
490 """Perform the STATUS command."""
491 fcp_dec = self._cmd.rs.status()
492 self._cmd.poutput_json(fcp_dec)
493
Harald Welte703f9332021-04-10 18:39:32 +0200494
Harald Weltef2e761c2021-04-11 11:56:44 +0200495option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
496 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200497argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200498
499global_group = option_parser.add_argument_group('General Options')
500global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200501 help='script with pySim-shell commands to be executed automatically at start-up')
502global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Harald Weltec8ff0262021-04-11 12:06:13 +0200503
504adm_group = global_group.add_mutually_exclusive_group()
505adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
506 help='ADM PIN used for provisioning (overwrites default)')
507adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
508 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100509
510
511if __name__ == '__main__':
512
513 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200514 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100515
Philipp Maier13e258d2021-04-08 17:48:49 +0200516 # If a script file is specified, be sure that it actually exists
517 if opts.script:
518 if not os.access(opts.script, os.R_OK):
519 print("Invalid script file!")
520 sys.exit(2)
521
Philipp Maier2b11c322021-03-17 12:37:39 +0100522 # Register csv-file as card data provider, either from specified CSV
523 # or from CSV file in home directory
524 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
525 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200526 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100527 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200528 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100529
Philipp Maierea95c392021-09-16 13:10:19 +0200530 # Init card reader driver
531 sl = init_reader(opts)
532 if sl is None:
533 exit(1)
534
535 # Create command layer
536 scc = SimCardCommands(transport=sl)
537
538 rs, card = init_card(sl)
539 if (rs is None or card is None):
540 exit(1)
541 app = PysimApp(card, rs, opts.script)
542 rs.select('MF', app)
543
Philipp Maier228c98e2021-03-10 20:14:06 +0100544 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200545 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100546 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
547 if pin_adm:
548 try:
549 card.verify_adm(h2b(pin_adm))
550 except Exception as e:
551 print(e)
552
Harald Welteb2edd142021-01-08 23:29:35 +0100553 app.cmdloop()