blob: a8471ff58d121192e5b0dd1c30412e2c5e7bde7f [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
31from optparse import OptionParser
Philipp Maier2b11c322021-03-17 12:37:39 +010032from pathlib import Path
Harald Welteb2edd142021-01-08 23:29:35 +010033
34from pySim.ts_51_011 import EF, DF, EF_SST_map, EF_AD_mode_map
35from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
36from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
37
38from pySim.exceptions import *
39from pySim.commands import SimCardCommands
40from pySim.cards import card_detect, Card
41from pySim.utils import h2b, swap_nibbles, rpad, h2s
Philipp Maier5d3e2592021-02-22 17:22:16 +010042from pySim.utils import dec_st, init_reader, sanitize_pin_adm, tabulate_str_list
Harald Welteb2edd142021-01-08 23:29:35 +010043from pySim.card_handler import card_handler
44
Philipp Maierff9dae22021-02-25 17:03:21 +010045from pySim.filesystem import CardMF, RuntimeState, CardDF, CardADF
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
48from pySim.ts_31_102 import ADF_USIM
49from pySim.ts_31_103 import ADF_ISIM
50
Philipp Maier2b11c322021-03-17 12:37:39 +010051from pySim.card_data import CardDataCsv, card_data_register, card_data_get_field
52
53
Harald Welteb2edd142021-01-08 23:29:35 +010054class PysimApp(cmd2.Cmd):
55 CUSTOM_CATEGORY = 'pySim Commands'
Philipp Maier681bc7b2021-03-10 19:52:41 +010056 def __init__(self, card, rs, script = None):
Harald Welteb2edd142021-01-08 23:29:35 +010057 basic_commands = [Iso7816Commands(), UsimCommands()]
58 super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
Philipp Maier681bc7b2021-03-10 19:52:41 +010059 use_ipython=True, auto_load_commands=False, command_sets=basic_commands, startup_script=script)
Harald Welteb2edd142021-01-08 23:29:35 +010060 self.intro = style('Welcome to pySim-shell!', fg=fg.red)
61 self.default_category = 'pySim-shell built-in commands'
62 self.card = card
Philipp Maier2b11c322021-03-17 12:37:39 +010063 iccid, sw = self.card.read_iccid()
64 self.iccid = iccid
Harald Welteb2edd142021-01-08 23:29:35 +010065 self.rs = rs
66 self.py_locals = { 'card': self.card, 'rs' : self.rs }
Harald Welteb2edd142021-01-08 23:29:35 +010067 self.numeric_path = False
68 self.add_settable(cmd2.Settable('numeric_path', bool, 'Print File IDs instead of names',
69 onchange_cb=self._onchange_numeric_path))
70 self.update_prompt()
71
72 def _onchange_numeric_path(self, param_name, old, new):
73 self.update_prompt()
74
75 def update_prompt(self):
76 path_list = self.rs.selected_file.fully_qualified_path(not self.numeric_path)
77 self.prompt = 'pySIM-shell (%s)> ' % ('/'.join(path_list))
78
79 @cmd2.with_category(CUSTOM_CATEGORY)
80 def do_intro(self, _):
81 """Display the intro banner"""
82 self.poutput(self.intro)
83
84 @cmd2.with_category(CUSTOM_CATEGORY)
85 def do_verify_adm(self, arg):
86 """VERIFY the ADM1 PIN"""
Philipp Maier2b11c322021-03-17 12:37:39 +010087 if arg:
88 # use specified ADM-PIN
89 pin_adm = sanitize_pin_adm(arg)
90 else:
91 # try to find an ADM-PIN if none is specified
92 result = card_data_get_field('ADM1', key='ICCID', value=self.iccid)
93 pin_adm = sanitize_pin_adm(result)
94 if pin_adm:
95 self.poutput("found adm-pin '%s' for ICCID '%s'" % (result, self.iccid))
96
97 if pin_adm:
98 self.card.verify_adm(h2b(pin_adm))
99 else:
100 self.poutput("error: cannot authenticate, no adm-pin!")
Harald Welteb2edd142021-01-08 23:29:35 +0100101
Philipp Maier2558aa62021-03-10 16:20:02 +0100102 @cmd2.with_category(CUSTOM_CATEGORY)
103 def do_desc(self, opts):
104 """Display human readable file description for the currently selected file"""
105 desc = self.rs.selected_file.desc
106 if desc:
107 self.poutput(desc)
108 else:
109 self.poutput("no description available")
Harald Welteb2edd142021-01-08 23:29:35 +0100110
111
112@with_default_category('ISO7816 Commands')
113class Iso7816Commands(CommandSet):
114 def __init__(self):
115 super().__init__()
116
117 def do_select(self, opts):
118 """SELECT a File (ADF/DF/EF)"""
Philipp Maierf62866f2021-03-10 17:13:15 +0100119 if len(opts.arg_list) == 0:
120 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
121 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
122 self._cmd.poutput("currently selected file: " + '/'.join(path_list) + " (" + '/'.join(path_list_fid) + ")")
123 return
124
Harald Welteb2edd142021-01-08 23:29:35 +0100125 path = opts.arg_list[0]
126 fcp_dec = self._cmd.rs.select(path, self._cmd)
127 self._cmd.update_prompt()
128 self._cmd.poutput(json.dumps(fcp_dec, indent=4))
129
130 def complete_select(self, text, line, begidx, endidx) -> List[str]:
131 """Command Line tab completion for SELECT"""
132 index_dict = { 1: self._cmd.rs.selected_file.get_selectable_names() }
133 return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
134
135 verify_chv_parser = argparse.ArgumentParser()
136 verify_chv_parser.add_argument('--chv-nr', type=int, default=1, help='CHV Number')
137 verify_chv_parser.add_argument('code', help='CODE/PIN/PUK')
138
139 @cmd2.with_argparser(verify_chv_parser)
140 def do_verify_chv(self, opts):
141 """Verify (authenticate) using specified CHV (PIN)"""
142 (data, sw) = self._cmd.card._scc.verify_chv(opts.chv_nr, opts.code)
143 self._cmd.poutput(data)
144
Philipp Maier5d3e2592021-02-22 17:22:16 +0100145 dir_parser = argparse.ArgumentParser()
146 dir_parser.add_argument('--fids', help='Show file identifiers', action='store_true')
147 dir_parser.add_argument('--names', help='Show file names', action='store_true')
148 dir_parser.add_argument('--apps', help='Show applications', action='store_true')
149 dir_parser.add_argument('--all', help='Show all selectable identifiers and names', action='store_true')
150
151 @cmd2.with_argparser(dir_parser)
152 def do_dir(self, opts):
153 """Show a listing of files available in currently selected DF or MF"""
154 if opts.all:
155 flags = []
156 elif opts.fids or opts.names or opts.apps:
157 flags = ['PARENT', 'SELF']
158 if opts.fids:
159 flags += ['FIDS', 'AIDS']
160 if opts.names:
161 flags += ['FNAMES', 'ANAMES']
162 if opts.apps:
163 flags += ['ANAMES', 'AIDS']
164 else:
165 flags = ['PARENT', 'SELF', 'FNAMES', 'ANAMES']
166 selectables = list(self._cmd.rs.selected_file.get_selectable_names(flags = flags))
167 directory_str = tabulate_str_list(selectables, width = 79, hspace = 2, lspace = 1, align_left = True)
168 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
169 self._cmd.poutput('/'.join(path_list))
170 path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
171 self._cmd.poutput('/'.join(path_list))
172 self._cmd.poutput(directory_str)
173 self._cmd.poutput("%d files" % len(selectables))
Harald Welteb2edd142021-01-08 23:29:35 +0100174
Philipp Maierff9dae22021-02-25 17:03:21 +0100175 def walk(self, indent = 0, action = None, context = None):
176 """Recursively walk through the file system, starting at the currently selected DF"""
177 files = self._cmd.rs.selected_file.get_selectables(flags = ['FNAMES', 'ANAMES'])
178 for f in files:
179 if not action:
180 output_str = " " * indent + str(f) + (" " * 250)
181 output_str = output_str[0:25]
182 if isinstance(files[f], CardADF):
183 output_str += " " + str(files[f].aid)
184 else:
185 output_str += " " + str(files[f].fid)
186 output_str += " " + str(files[f].desc)
187 self._cmd.poutput(output_str)
188 if isinstance(files[f], CardDF):
189 fcp_dec = self._cmd.rs.select(f, self._cmd)
190 self.walk(indent + 1, action, context)
191 fcp_dec = self._cmd.rs.select("..", self._cmd)
192 elif action:
193 action(f, context)
194
195 def do_tree(self, opts):
196 """Display a filesystem-tree with all selectable files"""
197 self.walk()
198
Philipp Maier24f7bd32021-02-25 17:06:18 +0100199 def export(self, filename, context):
200 context['COUNT'] += 1
201 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
202 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
203
204 self._cmd.poutput("#" * 80)
205 file_str = '/'.join(path_list) + "/" + str(filename) + " " * 80
206 self._cmd.poutput("# " + file_str[0:77] + "#")
207 self._cmd.poutput("#" * 80)
208
209 self._cmd.poutput("# directory: %s (%s)" % ('/'.join(path_list), '/'.join(path_list_fid)))
210 try:
211 fcp_dec = self._cmd.rs.select(filename, self._cmd)
212 path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
213 path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(False)
214 self._cmd.poutput("# file: %s (%s)" % (path_list[-1], path_list_fid[-1]))
215
216 fd = fcp_dec['file_descriptor']
217 structure = fd['structure']
218 self._cmd.poutput("# structure: %s" % str(structure))
219
220 for f in path_list:
221 self._cmd.poutput("select " + str(f))
222
223 if structure == 'transparent':
224 result = self._cmd.rs.read_binary()
225 self._cmd.poutput("update_binary " + str(result[0]))
226 if structure == 'cyclic' or structure == 'linear_fixed':
227 num_of_rec = fd['num_of_rec']
228 for r in range(1, num_of_rec + 1):
229 result = self._cmd.rs.read_record(r)
230 self._cmd.poutput("update_record %d %s" % (r, str(result[0])))
231 fcp_dec = self._cmd.rs.select("..", self._cmd)
232 except Exception as e:
233 bad_file_str = '/'.join(path_list) + "/" + str(filename) + ", " + str(e)
234 self._cmd.poutput("# bad file: %s" % bad_file_str)
235 context['ERR'] += 1
236 context['BAD'].append(bad_file_str)
237
238 self._cmd.poutput("#")
239
240 export_parser = argparse.ArgumentParser()
241 export_parser.add_argument('--filename', type=str, default=None, help='only export specific file')
242
243 @cmd2.with_argparser(export_parser)
244 def do_export(self, opts):
245 """Export files to script that can be imported back later"""
246 context = {'ERR':0, 'COUNT':0, 'BAD':[]}
247 if opts.filename:
248 self.export(opts.filename, context)
249 else:
250 self.walk(0, self.export, context)
251 self._cmd.poutput("# total files visited: %u" % context['COUNT'])
252 self._cmd.poutput("# bad files: %u" % context['ERR'])
253 for b in context['BAD']:
254 self._cmd.poutput("# " + b)
255 if context['ERR']:
256 raise RuntimeError("unable to export %i file(s)" % context['ERR'])
Harald Welteb2edd142021-01-08 23:29:35 +0100257
258
259@with_default_category('USIM Commands')
260class UsimCommands(CommandSet):
261 def __init__(self):
262 super().__init__()
263
264 def do_read_ust(self, _):
265 """Read + Display the EF.UST"""
266 self._cmd.card.select_adf_by_aid(adf="usim")
267 (res, sw) = self._cmd.card.read_ust()
268 self._cmd.poutput(res[0])
269 self._cmd.poutput(res[1])
270
271 def do_read_ehplmn(self, _):
272 """Read EF.EHPLMN"""
273 self._cmd.card.select_adf_by_aid(adf="usim")
274 (res, sw) = self._cmd.card.read_ehplmn()
275 self._cmd.poutput(res)
276
277def parse_options():
278
279 parser = OptionParser(usage="usage: %prog [options]")
280
281 parser.add_option("-d", "--device", dest="device", metavar="DEV",
282 help="Serial Device for SIM access [default: %default]",
283 default="/dev/ttyUSB0",
284 )
285 parser.add_option("-b", "--baud", dest="baudrate", type="int", metavar="BAUD",
286 help="Baudrate used for SIM access [default: %default]",
287 default=9600,
288 )
289 parser.add_option("-p", "--pcsc-device", dest="pcsc_dev", type='int', metavar="PCSC",
290 help="Which PC/SC reader number for SIM access",
291 default=None,
292 )
293 parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",
294 help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",
295 default=None,
296 )
297 parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",
298 help="Baudrate used for modem's port [default: %default]",
299 default=115200,
300 )
301 parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",
302 help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",
303 default=None,
304 )
Philipp Maier681bc7b2021-03-10 19:52:41 +0100305 parser.add_option("--script", dest="script", metavar="PATH",
306 help="script with shell commands to be executed automatically",
307 default=None,
308 )
Harald Welteb2edd142021-01-08 23:29:35 +0100309
Philipp Maier2b11c322021-03-17 12:37:39 +0100310 parser.add_option("--csv", dest="csv", metavar="FILE",
311 help="Read card data from CSV file",
312 default=None,
313 )
314
Harald Welteb2edd142021-01-08 23:29:35 +0100315 parser.add_option("-a", "--pin-adm", dest="pin_adm",
316 help="ADM PIN used for provisioning (overwrites default)",
317 )
318 parser.add_option("-A", "--pin-adm-hex", dest="pin_adm_hex",
319 help="ADM PIN used for provisioning, as hex string (16 characters long",
320 )
321
322 (options, args) = parser.parse_args()
323
324 if args:
325 parser.error("Extraneous arguments")
326
327 return options
328
329
330
331if __name__ == '__main__':
332
333 # Parse options
334 opts = parse_options()
335
336 # Init card reader driver
337 sl = init_reader(opts)
338 if (sl == None):
339 exit(1)
340
341 # Create command layer
342 scc = SimCardCommands(transport=sl)
343
344 sl.wait_for_card();
345
346 card_handler = card_handler(sl)
347
348 card = card_detect("auto", scc)
349 if card is None:
350 print("No card detected!")
351 sys.exit(2)
352
353 profile = CardProfileUICC()
Philipp Maier1e896f32021-03-10 17:02:53 +0100354 profile.add_application(ADF_USIM())
355 profile.add_application(ADF_ISIM())
356
Harald Welteb2edd142021-01-08 23:29:35 +0100357 rs = RuntimeState(card, profile)
358
359 # FIXME: do this dynamically
360 rs.mf.add_file(DF_TELECOM())
361 rs.mf.add_file(DF_GSM())
Harald Welteb2edd142021-01-08 23:29:35 +0100362
Philipp Maier681bc7b2021-03-10 19:52:41 +0100363 app = PysimApp(card, rs, opts.script)
Philipp Maier9c1a4ec2021-03-10 12:38:15 +0100364 rs.select('MF', app)
Philipp Maier228c98e2021-03-10 20:14:06 +0100365
Philipp Maier2b11c322021-03-17 12:37:39 +0100366 # Register csv-file as card data provider, either from specified CSV
367 # or from CSV file in home directory
368 csv_default = str(Path.home()) + "/.osmocom/pysim/card_data.csv"
369 if opts.csv:
370 card_data_register(CardDataCsv(opts.csv))
371 if os.path.isfile(csv_default):
372 card_data_register(CardDataCsv(csv_default))
373
Philipp Maier228c98e2021-03-10 20:14:06 +0100374 # If the user supplies an ADM PIN at via commandline args authenticate
375 # immediatley so that the user does not have to use the shell commands
376 pin_adm = sanitize_pin_adm(opts.pin_adm, opts.pin_adm_hex)
377 if pin_adm:
378 try:
379 card.verify_adm(h2b(pin_adm))
380 except Exception as e:
381 print(e)
382
Harald Welteb2edd142021-01-08 23:29:35 +0100383 app.cmdloop()