blob: 5aa311c0b27601596ed767682db6f4472af9d463 [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 Welte4c1dca02021-10-14 17:48:25 +020051# we need to import this module so that the SysmocomSJA2 sub-class of
52# CardModel is created, which will add the ATR-based matching and
53# calling of SysmocomSJA2.add_files. See CardModel.apply_matching_models
Harald Weltef44256c2021-10-14 15:53:39 +020054import pySim.sysmocom_sja2
55
Harald Welte4442b3d2021-04-03 09:00:16 +020056from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
Philipp Maier2b11c322021-03-17 12:37:39 +010057
Philipp Maierea95c392021-09-16 13:10:19 +020058def init_card(sl):
59 """
60 Detect card in reader and setup card profile and runtime state. This
61 function must be called at least once on startup. The card and runtime
62 state object (rs) is required for all pySim-shell commands.
63 """
64
65 # Wait up to three seconds for a card in reader and try to detect
66 # the card type.
67 print("Waiting for card...")
68 try:
69 sl.wait_for_card(3)
70 except NoCardError:
71 print("No card detected!")
72 return None, None;
73 except:
74 print("Card not readable!")
75 return None, None;
76
77 card = card_detect("auto", scc)
78 if card is None:
79 print("Could not detect card type!")
80 return None, None;
81
82 # Create runtime state with card profile
83 profile = CardProfileUICC()
84 profile.add_application(CardApplicationUSIM)
85 profile.add_application(CardApplicationISIM)
86 rs = RuntimeState(card, profile)
87
88 # FIXME: do this dynamically
89 rs.mf.add_file(DF_TELECOM())
90 rs.mf.add_file(DF_GSM())
91
Harald Weltef44256c2021-10-14 15:53:39 +020092 CardModel.apply_matching_models(scc, rs)
93
Philipp Maierea95c392021-09-16 13:10:19 +020094 # inform the transport that we can do context-specific SW interpretation
95 sl.set_sw_interpreter(rs)
96
97 return rs, card
Philipp Maier2b11c322021-03-17 12:37:39 +010098
Harald Welteb2edd142021-01-08 23:29:35 +010099class PysimApp(cmd2.Cmd):
100 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier681bc7b2021-03-10 19:52:41 +0100101 def __init__(self, card, rs, script = None):
Harald Welte31d2cf02021-04-03 10:47:29 +0200102 basic_commands = [Iso7816Commands(), PySimCommands()]
Harald Welteb2edd142021-01-08 23:29:35 +0100103 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier681bc7b2021-03-10 19:52:41 +0100104 use_ipython=True, auto_load_commands=False, command_sets=basic_commands, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +0100105 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
106 self.default_category = 'pySim-shell built-in commands'
107 self.card = card
Philipp Maier2b11c322021-03-17 12:37:39 +0100108 iccid, sw = self.card.read_iccid()
109 self.iccid = iccid
Harald Welteb2edd142021-01-08 23:29:35 +0100110 self.rs = rs
111 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Harald Welteb2edd142021-01-08 23:29:35 +0100112 self.numeric_path = False
113 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
114 onchange_cb=self._onchange_numeric_path))
Philipp Maier38c74f62021-03-17 17:19:52 +0100115 self.conserve_write = True
116 self.add_settable(cmd2.Settable('conserve_write', bool, 'Read and compare before write',
117 onchange_cb=self._onchange_conserve_write))
Harald Welteb2edd142021-01-08 23:29:35 +0100118 self.update_prompt()
Harald Welte1748b932021-04-06 21:12:25 +0200119 self.json_pretty_print = True
120 self.add_settable(cmd2.Settable('json_pretty_print', bool, 'Pretty-Print JSON output'))
Harald Welte7829d8a2021-04-10 11:28:53 +0200121 self.apdu_trace = False
122 self.add_settable(cmd2.Settable('apdu_trace', bool, 'Trace and display APDUs exchanged with card',
123 onchange_cb=self._onchange_apdu_trace))
Harald Welte1748b932021-04-06 21:12:25 +0200124
125 def poutput_json(self, data, force_no_pretty = False):
Harald Weltec9cdce32021-04-11 10:28:28 +0200126 """like cmd2.poutput() but for a JSON serializable dict."""
Harald Welte1748b932021-04-06 21:12:25 +0200127 if force_no_pretty or self.json_pretty_print == False:
Harald Welte5e749a72021-04-10 17:18:17 +0200128 output = json.dumps(data, cls=JsonEncoder)
Harald Welte1748b932021-04-06 21:12:25 +0200129 else:
Harald Welte5e749a72021-04-10 17:18:17 +0200130 output = json.dumps(data, cls=JsonEncoder, indent=4)
Harald Welte1748b932021-04-06 21:12:25 +0200131 self.poutput(output)
Harald Welteb2edd142021-01-08 23:29:35 +0100132
133 def _onchange_numeric_path(self, param_name, old, new):
134 self.update_prompt()
135
Philipp Maier38c74f62021-03-17 17:19:52 +0100136 def _onchange_conserve_write(self, param_name, old, new):
137 self.rs.conserve_write = new
138
Harald Welte7829d8a2021-04-10 11:28:53 +0200139 def _onchange_apdu_trace(self, param_name, old, new):
140 if new == True:
141 self.card._scc._tp.apdu_tracer = self.Cmd2ApduTracer(self)
142 else:
143 self.card._scc._tp.apdu_tracer = None
144
145 class Cmd2ApduTracer(ApduTracer):
146 def __init__(self, cmd2_app):
147 self.cmd2 = app
148
149 def trace_response(self, cmd, sw, resp):
150 self.cmd2.poutput("-> %s %s" % (cmd[:10], cmd[10:]))
151 self.cmd2.poutput("<- %s: %s" % (sw, resp))
152
Harald Welteb2edd142021-01-08 23:29:35 +0100153 def update_prompt(self):
154 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
155 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
156
157 @cmd2.with_category(CUSTOM_CATEGORY)
158 def do_intro(self, _):
159 """Display the intro banner"""
160 self.poutput(self.intro)
161
Harald Welteb2edd142021-01-08 23:29:35 +0100162
Harald Welte31d2cf02021-04-03 10:47:29 +0200163@with_default_category('pySim Commands')
164class PySimCommands(CommandSet):
Harald Welteb2edd142021-01-08 23:29:35 +0100165 def __init__(self):
166 super().__init__()
167
Philipp Maier5d3e2592021-02-22 17:22:16 +0100168 dir_parser = argparse.ArgumentParser()
169 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
170 dir_parser.add_argument('--names', help='Show file names', action='store_true')
171 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
172 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
173
174 @cmd2.with_argparser(dir_parser)
175 def do_dir(self, opts):
176 """Show a listing of files available in currently selected DF or MF"""
177 if opts.all:
178 flags = []
179 elif opts.fids or opts.names or opts.apps:
180 flags = ['PARENT', 'SELF']
181 if opts.fids:
182 flags += ['FIDS', 'AIDS']
183 if opts.names:
184 flags += ['FNAMES', 'ANAMES']
185 if opts.apps:
186 flags += ['ANAMES', 'AIDS']
187 else:
188 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
189 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
190 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
191 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
192 self._cmd.poutput('/'.join(path_list))
193 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
194 self._cmd.poutput('/'.join(path_list))
195 self._cmd.poutput(directory_str)
196 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100197
Philipp Maierff9dae22021-02-25 17:03:21 +0100198 def walk(self, indent = 0, action = None, context = None):
199 """Recursively walk through the file system, starting at the currently selected DF"""
200 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
201 for f in files:
202 if not action:
203 output_str = " " * indent + str(f) + (" " * 250)
204 output_str = output_str[0:25]
205 if isinstance(files[f], CardADF):
206 output_str += " " + str(files[f].aid)
207 else:
208 output_str += " " + str(files[f].fid)
209 output_str += " " + str(files[f].desc)
210 self._cmd.poutput(output_str)
Philipp Maierf408a402021-04-09 21:16:12 +0200211
Philipp Maierff9dae22021-02-25 17:03:21 +0100212 if isinstance(files[f], CardDF):
Philipp Maierf408a402021-04-09 21:16:12 +0200213 skip_df=False
214 try:
215 fcp_dec = self._cmd.rs.select(f, self._cmd)
216 except Exception as e:
217 skip_df=True
218 df = self._cmd.rs.selected_file
219 df_path_list = df.fully_qualified_path(True)
220 df_skip_reason_str = '/'.join(df_path_list) + "/" + str(f) + ", " + str(e)
221 if context:
222 context['DF_SKIP'] += 1
223 context['DF_SKIP_REASON'].append(df_skip_reason_str)
224
225 # If the DF was skipped, we never have entered the directory
226 # below, so we must not move up.
227 if skip_df == False:
228 self.walk(indent + 1, action, context)
229 fcp_dec = self._cmd.rs.select("..", self._cmd)
230
Philipp Maierff9dae22021-02-25 17:03:21 +0100231 elif action:
Philipp Maierb152a9e2021-04-01 17:13:03 +0200232 df_before_action = self._cmd.rs.selected_file
Philipp Maierff9dae22021-02-25 17:03:21 +0100233 action(f, context)
Philipp Maierb152a9e2021-04-01 17:13:03 +0200234 # When walking through the file system tree the action must not
235 # always restore the currently selected file to the file that
236 # was selected before executing the action() callback.
237 if df_before_action != self._cmd.rs.selected_file:
Harald Weltec9cdce32021-04-11 10:28:28 +0200238 raise RuntimeError("inconsistent walk, %s is currently selected but expecting %s to be selected"
Philipp Maierb152a9e2021-04-01 17:13:03 +0200239 % (str(self._cmd.rs.selected_file), str(df_before_action)))
Philipp Maierff9dae22021-02-25 17:03:21 +0100240
241 def do_tree(self, opts):
242 """Display a filesystem-tree with all selectable files"""
243 self.walk()
244
Philipp Maier24f7bd32021-02-25 17:06:18 +0100245 def export(self, filename, context):
Philipp Maierac34dcc2021-04-01 17:19:05 +0200246 """ Select and export a single file """
Philipp Maier24f7bd32021-02-25 17:06:18 +0100247 context['COUNT'] += 1
Philipp Maierac34dcc2021-04-01 17:19:05 +0200248 df = self._cmd.rs.selected_file
249
250 if not isinstance(df, CardDF):
251 raise RuntimeError("currently selected file %s is not a DF or ADF" % str(df))
252
253 df_path_list = df.fully_qualified_path(True)
254 df_path_list_fid = df.fully_qualified_path(False)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100255
Philipp Maier80ce71f2021-04-19 21:24:23 +0200256 file_str = '/'.join(df_path_list) + "/" + str(filename)
257 self._cmd.poutput(boxed_heading_str(file_str))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100258
Philipp Maierac34dcc2021-04-01 17:19:05 +0200259 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(df_path_list), '/'.join(df_path_list_fid)))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100260 try:
261 fcp_dec = self._cmd.rs.select(filename, self._cmd)
Philipp Maierac34dcc2021-04-01 17:19:05 +0200262 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 +0100263
264 fd = fcp_dec['file_descriptor']
265 structure = fd['structure']
266 self._cmd.poutput("# structure: %s" % str(structure))
267
Philipp Maierac34dcc2021-04-01 17:19:05 +0200268 for f in df_path_list:
Philipp Maier24f7bd32021-02-25 17:06:18 +0100269 self._cmd.poutput("select " + str(f))
Philipp Maierac34dcc2021-04-01 17:19:05 +0200270 self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100271
272 if structure == 'transparent':
273 result = self._cmd.rs.read_binary()
274 self._cmd.poutput("update_binary " + str(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200275 elif structure == 'cyclic' or structure == 'linear_fixed':
Philipp Maier24f7bd32021-02-25 17:06:18 +0100276 num_of_rec = fd['num_of_rec']
277 for r in range(1, num_of_rec + 1):
278 result = self._cmd.rs.read_record(r)
279 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
Harald Welte917d98c2021-04-21 11:51:25 +0200280 elif structure == 'ber_tlv':
281 tags = self._cmd.rs.retrieve_tags()
282 for t in tags:
283 result = self._cmd.rs.retrieve_data(t)
Harald Weltec1475302021-05-21 21:47:55 +0200284 (tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
Harald Welte917d98c2021-04-21 11:51:25 +0200285 self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
286 else:
287 raise RuntimeError('Unsupported structure "%s" of file "%s"' % (structure, filename))
Philipp Maier24f7bd32021-02-25 17:06:18 +0100288 except Exception as e:
Philipp Maierac34dcc2021-04-01 17:19:05 +0200289 bad_file_str = '/'.join(df_path_list) + "/" + str(filename) + ", " + str(e)
Philipp Maier24f7bd32021-02-25 17:06:18 +0100290 self._cmd.poutput("# bad file: %s" % bad_file_str)
291 context['ERR'] += 1
292 context['BAD'].append(bad_file_str)
293
Philipp Maierac34dcc2021-04-01 17:19:05 +0200294 # When reading the file is done, make sure the parent file is
295 # selected again. This will be the usual case, however we need
296 # to check before since we must not select the same DF twice
297 if df != self._cmd.rs.selected_file:
298 self._cmd.rs.select(df.fid or df.aid, self._cmd)
299
Philipp Maier24f7bd32021-02-25 17:06:18 +0100300 self._cmd.poutput("#")
301
302 export_parser = argparse.ArgumentParser()
303 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
304
305 @cmd2.with_argparser(export_parser)
306 def do_export(self, opts):
307 """Export files to script that can be imported back later"""
Philipp Maierf408a402021-04-09 21:16:12 +0200308 context = {'ERR':0, 'COUNT':0, 'BAD':[], 'DF_SKIP':0, 'DF_SKIP_REASON':[]}
Philipp Maier24f7bd32021-02-25 17:06:18 +0100309 if opts.filename:
310 self.export(opts.filename, context)
311 else:
312 self.walk(0, self.export, context)
Philipp Maier80ce71f2021-04-19 21:24:23 +0200313
314 self._cmd.poutput(boxed_heading_str("Export summary"))
315
Philipp Maier24f7bd32021-02-25 17:06:18 +0100316 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
317 self._cmd.poutput("# bad files: %u" % context['ERR'])
318 for b in context['BAD']:
319 self._cmd.poutput("# " + b)
Philipp Maierf408a402021-04-09 21:16:12 +0200320
321 self._cmd.poutput("# skipped dedicated files(s): %u" % context['DF_SKIP'])
322 for b in context['DF_SKIP_REASON']:
323 self._cmd.poutput("# " + b)
324
325 if context['ERR'] and context['DF_SKIP']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200326 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 +0200327 elif context['ERR']:
Harald Weltec9cdce32021-04-11 10:28:28 +0200328 raise RuntimeError("unable to export %i elementary file(s)" % context['ERR'])
Philipp Maierf408a402021-04-09 21:16:12 +0200329 elif context['DF_SKIP']:
330 raise RuntimeError("unable to export %i dedicated files(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100331
Harald Weltedaf2b392021-05-03 23:17:29 +0200332 def do_reset(self, opts):
333 """Reset the Card."""
334 atr = self._cmd.rs.reset(self._cmd)
335 self._cmd.poutput('Card ATR: %s' % atr)
336 self._cmd.update_prompt()
337
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200338 def do_desc(self, opts):
339 """Display human readable file description for the currently selected file"""
340 desc = self._cmd.rs.selected_file.desc
341 if desc:
342 self._cmd.poutput(desc)
343 else:
344 self._cmd.poutput("no description available")
345
346 def do_verify_adm(self, arg):
347 """VERIFY the ADM1 PIN"""
348 if arg:
349 # use specified ADM-PIN
350 pin_adm = sanitize_pin_adm(arg)
351 else:
352 # try to find an ADM-PIN if none is specified
353 result = card_key_provider_get_field('ADM1', key='ICCID', value=self._cmd.iccid)
354 pin_adm = sanitize_pin_adm(result)
355 if pin_adm:
356 self._cmd.poutput("found ADM-PIN '%s' for ICCID '%s'" % (result, self._cmd.iccid))
357 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200358 raise ValueError("cannot find ADM-PIN for ICCID '%s'" % (self._cmd.iccid))
Philipp Maiera8c9ea92021-09-16 12:51:46 +0200359
360 if pin_adm:
361 self._cmd.card.verify_adm(h2b(pin_adm))
362 else:
Philipp Maierf0241452021-09-22 16:35:55 +0200363 raise ValueError("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100364
Harald Welte31d2cf02021-04-03 10:47:29 +0200365@with_default_category('ISO7816 Commands')
366class Iso7816Commands(CommandSet):
367 def __init__(self):
368 super().__init__()
369
370 def do_select(self, opts):
371 """SELECT a File (ADF/DF/EF)"""
372 if len(opts.arg_list) == 0:
373 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
374 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
375 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
376 return
377
378 path = opts.arg_list[0]
379 fcp_dec = self._cmd.rs.select(path, self._cmd)
380 self._cmd.update_prompt()
Harald Welteb00e8932021-04-10 17:19:13 +0200381 self._cmd.poutput_json(fcp_dec)
Harald Welte31d2cf02021-04-03 10:47:29 +0200382
383 def complete_select(self, text, line, begidx, endidx) -> List[str]:
384 """Command Line tab completion for SELECT"""
385 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
386 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
387
388 def get_code(self, code):
389 """Use code either directly or try to get it from external data source"""
390 auto = ('PIN1', 'PIN2', 'PUK1', 'PUK2')
391
392 if str(code).upper() not in auto:
393 return sanitize_pin_adm(code)
394
395 result = card_key_provider_get_field(str(code), key='ICCID', value=self._cmd.iccid)
396 result = sanitize_pin_adm(result)
397 if result:
398 self._cmd.poutput("found %s '%s' for ICCID '%s'" % (code.upper(), result, self._cmd.iccid))
399 else:
400 self._cmd.poutput("cannot find %s for ICCID '%s'" % (code.upper(), self._cmd.iccid))
401 return result
402
403 verify_chv_parser = argparse.ArgumentParser()
404 verify_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
405 verify_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
406
407 @cmd2.with_argparser(verify_chv_parser)
408 def do_verify_chv(self, opts):
409 """Verify (authenticate) using specified PIN code"""
410 pin = self.get_code(opts.pin_code)
411 (data, sw) = self._cmd.card._scc.verify_chv(opts.pin_nr, h2b(pin))
Harald Weltec9cdce32021-04-11 10:28:28 +0200412 self._cmd.poutput("CHV verification successful")
Harald Welte31d2cf02021-04-03 10:47:29 +0200413
414 unblock_chv_parser = argparse.ArgumentParser()
415 unblock_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
416 unblock_chv_parser.add_argument('puk_code', type=str, help='PUK code digits \"PUK1\" or \"PUK2\" to get PUK code from external data source')
417 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')
418
419 @cmd2.with_argparser(unblock_chv_parser)
420 def do_unblock_chv(self, opts):
421 """Unblock PIN code using specified PUK code"""
422 new_pin = self.get_code(opts.new_pin_code)
423 puk = self.get_code(opts.puk_code)
424 (data, sw) = self._cmd.card._scc.unblock_chv(opts.pin_nr, h2b(puk), h2b(new_pin))
425 self._cmd.poutput("CHV unblock successful")
426
427 change_chv_parser = argparse.ArgumentParser()
428 change_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PUK Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
429 change_chv_parser.add_argument('pin_code', type=str, help='PIN code digits \"PIN1\" or \"PIN2\" to get PIN code from external data source')
430 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')
431
432 @cmd2.with_argparser(change_chv_parser)
433 def do_change_chv(self, opts):
434 """Change PIN code to a new PIN code"""
435 new_pin = self.get_code(opts.new_pin_code)
436 pin = self.get_code(opts.pin_code)
437 (data, sw) = self._cmd.card._scc.change_chv(opts.pin_nr, h2b(pin), h2b(new_pin))
438 self._cmd.poutput("CHV change successful")
439
440 disable_chv_parser = argparse.ArgumentParser()
441 disable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
442 disable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
443
444 @cmd2.with_argparser(disable_chv_parser)
445 def do_disable_chv(self, opts):
446 """Disable PIN code using specified PIN code"""
447 pin = self.get_code(opts.pin_code)
448 (data, sw) = self._cmd.card._scc.disable_chv(opts.pin_nr, h2b(pin))
449 self._cmd.poutput("CHV disable successful")
450
451 enable_chv_parser = argparse.ArgumentParser()
452 enable_chv_parser.add_argument('--pin-nr', type=int, default=1, help='PIN Number, 1=PIN1, 2=PIN2 or custom value (decimal)')
453 enable_chv_parser.add_argument('pin_code', type=str, help='PIN code digits, \"PIN1\" or \"PIN2\" to get PIN code from external data source')
454
455 @cmd2.with_argparser(enable_chv_parser)
456 def do_enable_chv(self, opts):
457 """Enable PIN code using specified PIN code"""
458 pin = self.get_code(opts.pin_code)
459 (data, sw) = self._cmd.card._scc.enable_chv(opts.pin_nr, h2b(pin))
460 self._cmd.poutput("CHV enable successful")
461
Harald Weltea4631612021-04-10 18:17:55 +0200462 def do_deactivate_file(self, opts):
463 """Deactivate the current EF"""
Harald Welte485692b2021-05-25 22:21:44 +0200464 (data, sw) = self._cmd.card._scc.deactivate_file()
Harald Weltea4631612021-04-10 18:17:55 +0200465
466 def do_activate_file(self, opts):
Harald Welte485692b2021-05-25 22:21:44 +0200467 """Activate the specified EF"""
468 path = opts.arg_list[0]
469 (data, sw) = self._cmd.rs.activate_file(path)
470
471 def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
472 """Command Line tab completion for ACTIVATE FILE"""
473 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
474 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
Harald Welte31d2cf02021-04-03 10:47:29 +0200475
Harald Welte703f9332021-04-10 18:39:32 +0200476 open_chan_parser = argparse.ArgumentParser()
477 open_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
478
479 @cmd2.with_argparser(open_chan_parser)
480 def do_open_channel(self, opts):
481 """Open a logical channel."""
482 (data, sw) = self._cmd.card._scc.manage_channel(mode='open', lchan_nr=opts.chan_nr)
483
484 close_chan_parser = argparse.ArgumentParser()
485 close_chan_parser.add_argument('chan_nr', type=int, default=0, help='Channel Number')
486
487 @cmd2.with_argparser(close_chan_parser)
488 def do_close_channel(self, opts):
489 """Close a logical channel."""
490 (data, sw) = self._cmd.card._scc.manage_channel(mode='close', lchan_nr=opts.chan_nr)
491
Harald Welte34b05d32021-05-25 22:03:13 +0200492 def do_status(self, opts):
493 """Perform the STATUS command."""
494 fcp_dec = self._cmd.rs.status()
495 self._cmd.poutput_json(fcp_dec)
496
Harald Welte703f9332021-04-10 18:39:32 +0200497
Harald Weltef2e761c2021-04-11 11:56:44 +0200498option_parser = argparse.ArgumentParser(prog='pySim-shell', description='interactive SIM card shell',
499 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
Harald Welte28c24312021-04-11 12:19:36 +0200500argparse_add_reader_args(option_parser)
Harald Weltec8ff0262021-04-11 12:06:13 +0200501
502global_group = option_parser.add_argument_group('General Options')
503global_group.add_argument('--script', metavar='PATH', default=None,
Harald Welte28c24312021-04-11 12:19:36 +0200504 help='script with pySim-shell commands to be executed automatically at start-up')
505global_group.add_argument('--csv', metavar='FILE', default=None, help='Read card data from CSV file')
Harald Weltec8ff0262021-04-11 12:06:13 +0200506
507adm_group = global_group.add_mutually_exclusive_group()
508adm_group.add_argument('-a', '--pin-adm', metavar='PIN_ADM1', dest='pin_adm', default=None,
509 help='ADM PIN used for provisioning (overwrites default)')
510adm_group.add_argument('-A', '--pin-adm-hex', metavar='PIN_ADM1_HEX', dest='pin_adm_hex', default=None,
511 help='ADM PIN used for provisioning, as hex string (16 characters long)')
Harald Welteb2edd142021-01-08 23:29:35 +0100512
513
514if __name__ == '__main__':
515
516 # Parse options
Harald Weltef2e761c2021-04-11 11:56:44 +0200517 opts = option_parser.parse_args()
Harald Welteb2edd142021-01-08 23:29:35 +0100518
Philipp Maier13e258d2021-04-08 17:48:49 +0200519 # If a script file is specified, be sure that it actually exists
520 if opts.script:
521 if not os.access(opts.script, os.R_OK):
522 print("Invalid script file!")
523 sys.exit(2)
524
Philipp Maier2b11c322021-03-17 12:37:39 +0100525 # Register csv-file as card data provider, either from specified CSV
526 # or from CSV file in home directory
527 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
528 if opts.csv:
Harald Welte4442b3d2021-04-03 09:00:16 +0200529 card_key_provider_register(CardKeyProviderCsv(opts.csv))
Philipp Maier2b11c322021-03-17 12:37:39 +0100530 if os.path.isfile(csv_default):
Harald Welte4442b3d2021-04-03 09:00:16 +0200531 card_key_provider_register(CardKeyProviderCsv(csv_default))
Philipp Maier2b11c322021-03-17 12:37:39 +0100532
Philipp Maierea95c392021-09-16 13:10:19 +0200533 # Init card reader driver
534 sl = init_reader(opts)
535 if sl is None:
536 exit(1)
537
538 # Create command layer
539 scc = SimCardCommands(transport=sl)
540
541 rs, card = init_card(sl)
542 if (rs is None or card is None):
543 exit(1)
544 app = PysimApp(card, rs, opts.script)
545 rs.select('MF', app)
546
Philipp Maier228c98e2021-03-10 20:14:06 +0100547 # If the user supplies an ADM PIN at via commandline args authenticate
Harald Weltec9cdce32021-04-11 10:28:28 +0200548 # immediately so that the user does not have to use the shell commands
Philipp Maier228c98e2021-03-10 20:14:06 +0100549 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
550 if pin_adm:
551 try:
552 card.verify_adm(h2b(pin_adm))
553 except Exception as e:
554 print(e)
555
Harald Welteb2edd142021-01-08 23:29:35 +0100556 app.cmdloop()