Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Interactive shell for working with SIM / UICC / USIM / ISIM cards |
| 4 | # |
| 5 | # (C) 2022 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 | |
| 20 | from typing import List |
| 21 | |
| 22 | import cmd2 |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 23 | from cmd2 import CommandSet, with_default_category, with_argparser |
| 24 | import argparse |
| 25 | |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 26 | from pySim.exceptions import * |
| 27 | from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder |
| 28 | |
| 29 | from pySim.ts_102_221 import * |
| 30 | |
| 31 | @with_default_category('TS 102 222 Administrative Commands') |
| 32 | class Ts102222Commands(CommandSet): |
| 33 | """Administrative commands for telecommunication applications.""" |
| 34 | |
| 35 | def __init__(self): |
| 36 | super().__init__() |
| 37 | |
| 38 | delfile_parser = argparse.ArgumentParser() |
| 39 | delfile_parser.add_argument('--force-delete', action='store_true', |
| 40 | help='I really want to permanently delete the file. I know pySim cannot re-create it yet!') |
| 41 | delfile_parser.add_argument('NAME', type=str, help='File name or FID to delete') |
| 42 | |
| 43 | @cmd2.with_argparser(delfile_parser) |
| 44 | def do_delete_file(self, opts): |
| 45 | """Delete the specified file. DANGEROUS! See TS 102 222 Section 6.4. |
| 46 | This will permanently delete the specified file from the card. |
| 47 | pySim has no support to re-create files yet, and even if it did, your card may not allow it!""" |
| 48 | if not opts.force_delete: |
| 49 | self._cmd.perror("Refusing to permanently delete the file, please read the help text.") |
| 50 | return |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 51 | f = self._cmd.lchan.get_file_for_selectable(opts.NAME) |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 52 | (data, sw) = self._cmd.card._scc.delete_file(f.fid) |
| 53 | |
| 54 | def complete_delete_file(self, text, line, begidx, endidx) -> List[str]: |
| 55 | """Command Line tab completion for DELETE FILE""" |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 56 | index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()} |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 57 | return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) |
| 58 | |
| 59 | termdf_parser = argparse.ArgumentParser() |
| 60 | termdf_parser.add_argument('--force', action='store_true', |
| 61 | help='I really want to terminate the file. I know I can not recover from it!') |
| 62 | termdf_parser.add_argument('NAME', type=str, help='File name or FID') |
| 63 | |
| 64 | @cmd2.with_argparser(termdf_parser) |
| 65 | def do_terminate_df(self, opts): |
| 66 | """Terminate the specified DF. DANGEROUS! See TS 102 222 6.7. |
| 67 | This is a permanent, one-way operation on the card. There is no undo, you can not recover |
| 68 | a terminated DF. The only permitted command for a terminated DF is the DLETE FILE command.""" |
| 69 | if not opts.force: |
| 70 | self._cmd.perror("Refusing to terminate the file, please read the help text.") |
| 71 | return |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 72 | f = self._cmd.lchan.get_file_for_selectable(opts.NAME) |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 73 | (data, sw) = self._cmd.card._scc.terminate_df(f.fid) |
| 74 | |
| 75 | def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]: |
| 76 | """Command Line tab completion for TERMINATE DF""" |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 77 | index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()} |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 78 | return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) |
| 79 | |
| 80 | @cmd2.with_argparser(termdf_parser) |
| 81 | def do_terminate_ef(self, opts): |
| 82 | """Terminate the specified EF. DANGEROUS! See TS 102 222 6.8. |
| 83 | This is a permanent, one-way operation on the card. There is no undo, you can not recover |
| 84 | a terminated EF. The only permitted command for a terminated EF is the DLETE FILE command.""" |
| 85 | if not opts.force: |
| 86 | self._cmd.perror("Refusing to terminate the file, please read the help text.") |
| 87 | return |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 88 | f = self._cmd.lchan.get_file_for_selectable(opts.NAME) |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 89 | (data, sw) = self._cmd.card._scc.terminate_ef(f.fid) |
| 90 | |
| 91 | def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]: |
| 92 | """Command Line tab completion for TERMINATE EF""" |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 93 | index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()} |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 94 | return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) |
| 95 | |
| 96 | tcard_parser = argparse.ArgumentParser() |
| 97 | tcard_parser.add_argument('--force-terminate-card', action='store_true', |
| 98 | help='I really want to permanently terminate the card. It will not be usable afterwards!') |
| 99 | |
| 100 | @cmd2.with_argparser(tcard_parser) |
| 101 | def do_terminate_card_usage(self, opts): |
| 102 | """Terminate the Card. SUPER DANGEROUS! See TS 102 222 Section 6.9. |
| 103 | This will permanently brick the card and can NOT be recovered from!""" |
| 104 | if not opts.force_terminate_card: |
| 105 | self._cmd.perror("Refusing to permanently terminate the card, please read the help text.") |
| 106 | return |
| 107 | (data, sw) = self._cmd.card._scc.terminate_card_usage() |
| 108 | |
| 109 | create_parser = argparse.ArgumentParser() |
| 110 | create_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string') |
| 111 | create_parser._action_groups.pop() |
| 112 | create_required = create_parser.add_argument_group('required arguments') |
| 113 | create_optional = create_parser.add_argument_group('optional arguments') |
| 114 | create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR') |
| 115 | create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR') |
| 116 | create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets') |
| 117 | create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'], |
| 118 | help='Structure of the to-be-created EF') |
| 119 | create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string') |
| 120 | create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?') |
| 121 | create_optional.add_argument('--record-length', type=int, help='Length of each record in octets') |
| 122 | |
| 123 | @cmd2.with_argparser(create_parser) |
| 124 | def do_create_ef(self, opts): |
| 125 | """Create a new EF below the currently selected DF. Requires related privileges.""" |
| 126 | file_descriptor = { |
| 127 | 'file_descriptor_byte': { |
| 128 | 'shareable': opts.shareable, |
| 129 | 'file_type': 'working_ef', |
| 130 | 'structure': opts.structure, |
| 131 | } |
| 132 | } |
| 133 | if opts.structure == 'linear_fixed': |
| 134 | if not opts.record_length: |
| 135 | self._cmd.perror("you must specify the --record-length for linear fixed EF") |
| 136 | return |
| 137 | file_descriptor['record_len'] = opts.record_length |
Christian Amsüss | e17e277 | 2022-04-19 10:29:09 +0200 | [diff] [blame] | 138 | file_descriptor['num_of_rec'] = opts.file_size // opts.record_length |
| 139 | if file_descriptor['num_of_rec'] * file_descriptor['record_len'] != opts.file_size: |
| 140 | raise ValueError("File size not evenly divisible by record length") |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 141 | elif opts.structure == 'ber_tlv': |
| 142 | self._cmd.perror("BER-TLV creation not yet fully supported, sorry") |
| 143 | return |
| 144 | ies = [FileDescriptor(decoded=file_descriptor), FileIdentifier(decoded=opts.FILE_ID), |
| 145 | LifeCycleStatusInteger(decoded='operational_activated'), |
| 146 | SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id, |
| 147 | 'ef_arr_record_nr': opts.ef_arr_record_nr }), |
| 148 | FileSize(decoded=opts.file_size), |
| 149 | ShortFileIdentifier(decoded=opts.short_file_id), |
| 150 | ] |
| 151 | fcp = FcpTemplate(children=ies) |
| 152 | (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv())) |
| 153 | # the newly-created file is automatically selected but our runtime state knows nothing of it |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 154 | self._cmd.lchan.select_file(self._cmd.lchan.selected_file) |
Harald Welte | 3c9b784 | 2021-10-19 21:44:24 +0200 | [diff] [blame] | 155 | |
| 156 | createdf_parser = argparse.ArgumentParser() |
| 157 | createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string') |
| 158 | createdf_parser._action_groups.pop() |
| 159 | createdf_required = createdf_parser.add_argument_group('required arguments') |
| 160 | createdf_optional = createdf_parser.add_argument_group('optional arguments') |
| 161 | createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments') |
| 162 | createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR') |
| 163 | createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR') |
| 164 | createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?') |
| 165 | createdf_optional.add_argument('--aid', type=str, help='Application ID (creates an ADF, instead of a DF)') |
| 166 | # mandatory by spec, but ignored by several OS, so don't force the user |
| 167 | createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets') |
| 168 | createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true') |
| 169 | createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true') |
| 170 | createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true') |
| 171 | createdf_sja_optional.add_argument('--permit-other-applet-delete-terminate', action='store_true') |
| 172 | |
| 173 | @cmd2.with_argparser(createdf_parser) |
| 174 | def do_create_df(self, opts): |
| 175 | """Create a new DF below the currently selected DF. Requires related privileges.""" |
| 176 | file_descriptor = { |
| 177 | 'file_descriptor_byte': { |
| 178 | 'shareable': opts.shareable, |
| 179 | 'file_type': 'df', |
| 180 | 'structure': 'no_info_given', |
| 181 | } |
| 182 | } |
| 183 | ies = [] |
| 184 | ies.append(FileDescriptor(decoded=file_descriptor)) |
| 185 | ies.append(FileIdentifier(decoded=opts.FILE_ID)) |
| 186 | if opts.aid: |
| 187 | ies.append(DfName(decoded=opts.aid)) |
| 188 | ies.append(LifeCycleStatusInteger(decoded='operational_activated')) |
| 189 | ies.append(SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id, |
| 190 | 'ef_arr_record_nr': opts.ef_arr_record_nr })) |
| 191 | if opts.total_file_size: |
| 192 | ies.append(TotalFileSize(decoded=opts.total_file_size)) |
| 193 | # TODO: Spec states PIN Status Template DO is mandatory |
| 194 | if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate: |
| 195 | toolkit_ac = { |
| 196 | 'rfm_create': opts.permit_rfm_create, |
| 197 | 'rfm_delete_terminate': opts.permit_rfm_delete_terminate, |
| 198 | 'other_applet_create': opts.permit_other_applet_create, |
| 199 | 'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate, |
| 200 | } |
| 201 | ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)])) |
| 202 | fcp = FcpTemplate(children=ies) |
| 203 | (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv())) |
| 204 | # the newly-created file is automatically selected but our runtime state knows nothing of it |
Harald Welte | a6c0f88 | 2022-07-17 14:23:17 +0200 | [diff] [blame] | 205 | self._cmd.lchan.select_file(self._cmd.lchan.selected_file) |
Harald Welte | 0707b80 | 2023-03-07 11:43:37 +0100 | [diff] [blame] | 206 | |
| 207 | resize_ef_parser = argparse.ArgumentParser() |
| 208 | resize_ef_parser.add_argument('NAME', type=str, help='Name or FID of file to be resized') |
| 209 | resize_ef_parser._action_groups.pop() |
| 210 | resize_ef_required = resize_ef_parser.add_argument_group('required arguments') |
| 211 | resize_ef_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets') |
| 212 | |
| 213 | @cmd2.with_argparser(resize_ef_parser) |
| 214 | def do_resize_ef(self, opts): |
| 215 | """Resize an existing EF below the currently selected DF. Requires related privileges.""" |
| 216 | f = self._cmd.lchan.get_file_for_selectable(opts.NAME) |
| 217 | ies = [FileIdentifier(decoded=f.fid), |
| 218 | FileSize(decoded=opts.file_size)] |
| 219 | fcp = FcpTemplate(children=ies) |
| 220 | (data, sw) = self._cmd.card._scc.resize_file(b2h(fcp.to_tlv())) |
| 221 | # the resized file is automatically selected but our runtime state knows nothing of it |
| 222 | self._cmd.lchan.select_file(self._cmd.lchan.selected_file) |
| 223 | |
| 224 | def complete_resize_ef(self, text, line, begidx, endidx) -> List[str]: |
| 225 | """Command Line tab completion for RESIZE EF""" |
| 226 | index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()} |
| 227 | return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) |