pySim-shell: support TS 102 222 administrative commands

This adds support for creating/deleting and terminating files,
as well as support for permanent card termination.

Change-Id: I5b1ffb1334afa18d62beb642268066a30deb7ea6
diff --git a/pySim/ts_102_222.py b/pySim/ts_102_222.py
new file mode 100644
index 0000000..2c1d600
--- /dev/null
+++ b/pySim/ts_102_222.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+
+# Interactive shell for working with SIM / UICC / USIM / ISIM cards
+#
+# (C) 2022 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from typing import List
+
+import cmd2
+from cmd2 import style, fg, bg
+from cmd2 import CommandSet, with_default_category, with_argparser
+import argparse
+
+from pySim.ts_31_102 import EF_UST_map, EF_USIM_ADF_map
+from pySim.ts_31_103 import EF_IST_map, EF_ISIM_ADF_map
+
+from pySim.exceptions import *
+from pySim.utils import h2b, swap_nibbles, b2h, JsonEncoder
+
+from pySim.ts_102_221 import *
+
+@with_default_category('TS 102 222 Administrative Commands')
+class Ts102222Commands(CommandSet):
+    """Administrative commands for telecommunication applications."""
+
+    def __init__(self):
+        super().__init__()
+
+    delfile_parser = argparse.ArgumentParser()
+    delfile_parser.add_argument('--force-delete', action='store_true',
+            help='I really want to permanently delete the file. I know pySim cannot re-create it yet!')
+    delfile_parser.add_argument('NAME', type=str, help='File name or FID to delete')
+
+    @cmd2.with_argparser(delfile_parser)
+    def do_delete_file(self, opts):
+        """Delete the specified file. DANGEROUS!  See TS 102 222 Section 6.4.
+        This will permanently delete the specified file from the card.
+        pySim has no support to re-create files yet, and even if it did, your card may not allow it!"""
+        if not opts.force_delete:
+            self._cmd.perror("Refusing to permanently delete the file, please read the help text.")
+            return
+        f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+        (data, sw) = self._cmd.card._scc.delete_file(f.fid)
+
+    def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
+        """Command Line tab completion for DELETE FILE"""
+        index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+        return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
+
+    termdf_parser = argparse.ArgumentParser()
+    termdf_parser.add_argument('--force', action='store_true',
+            help='I really want to terminate the file. I know I can not recover from it!')
+    termdf_parser.add_argument('NAME', type=str, help='File name or FID')
+
+    @cmd2.with_argparser(termdf_parser)
+    def do_terminate_df(self, opts):
+        """Terminate the specified DF. DANGEROUS!  See TS 102 222 6.7.
+        This is a permanent, one-way operation on the card. There is no undo, you can not recover
+        a terminated DF.  The only permitted command for a terminated DF is the DLETE FILE command."""
+        if not opts.force:
+            self._cmd.perror("Refusing to terminate the file, please read the help text.")
+            return
+        f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+        (data, sw) = self._cmd.card._scc.terminate_df(f.fid)
+
+    def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
+        """Command Line tab completion for TERMINATE DF"""
+        index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+        return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
+
+    @cmd2.with_argparser(termdf_parser)
+    def do_terminate_ef(self, opts):
+        """Terminate the specified EF. DANGEROUS!  See TS 102 222 6.8.
+        This is a permanent, one-way operation on the card. There is no undo, you can not recover
+        a terminated EF.  The only permitted command for a terminated EF is the DLETE FILE command."""
+        if not opts.force:
+            self._cmd.perror("Refusing to terminate the file, please read the help text.")
+            return
+        f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+        (data, sw) = self._cmd.card._scc.terminate_ef(f.fid)
+
+    def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
+        """Command Line tab completion for TERMINATE EF"""
+        index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+        return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict)
+
+    tcard_parser = argparse.ArgumentParser()
+    tcard_parser.add_argument('--force-terminate-card', action='store_true',
+            help='I really want to permanently terminate the card. It will not be usable afterwards!')
+
+    @cmd2.with_argparser(tcard_parser)
+    def do_terminate_card_usage(self, opts):
+        """Terminate the Card. SUPER DANGEROUS!  See TS 102 222 Section 6.9.
+        This will permanently brick the card and can NOT be recovered from!"""
+        if not opts.force_terminate_card:
+            self._cmd.perror("Refusing to permanently terminate the card, please read the help text.")
+            return
+        (data, sw) = self._cmd.card._scc.terminate_card_usage()
+
+    create_parser = argparse.ArgumentParser()
+    create_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
+    create_parser._action_groups.pop()
+    create_required = create_parser.add_argument_group('required arguments')
+    create_optional = create_parser.add_argument_group('optional arguments')
+    create_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
+    create_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
+    create_required.add_argument('--file-size', required=True, type=int, help='Size of file in octets')
+    create_required.add_argument('--structure', required=True, type=str, choices=['transparent', 'linear_fixed', 'ber_tlv'],
+                                 help='Structure of the to-be-created EF')
+    create_optional.add_argument('--short-file-id', type=str, help='Short File Identifier as 2-digit hex string')
+    create_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
+    create_optional.add_argument('--record-length', type=int, help='Length of each record in octets')
+
+    @cmd2.with_argparser(create_parser)
+    def do_create_ef(self, opts):
+        """Create a new EF below the currently selected DF.  Requires related privileges."""
+        file_descriptor = {
+            'file_descriptor_byte': {
+                'shareable': opts.shareable,
+                'file_type': 'working_ef',
+                'structure': opts.structure,
+            }
+        }
+        if opts.structure == 'linear_fixed':
+            if not opts.record_length:
+                self._cmd.perror("you must specify the --record-length for linear fixed EF")
+                return
+            file_descriptor['record_len'] = opts.record_length
+        elif opts.structure == 'ber_tlv':
+            self._cmd.perror("BER-TLV creation not yet fully supported, sorry")
+            return
+        ies = [FileDescriptor(decoded=file_descriptor), FileIdentifier(decoded=opts.FILE_ID),
+               LifeCycleStatusInteger(decoded='operational_activated'),
+               SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id,
+                                                 'ef_arr_record_nr': opts.ef_arr_record_nr }),
+               FileSize(decoded=opts.file_size),
+               ShortFileIdentifier(decoded=opts.short_file_id),
+            ]
+        fcp = FcpTemplate(children=ies)
+        (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
+        # the newly-created file is automatically selected but our runtime state knows nothing of it
+        self._cmd.rs.select_file(self._cmd.rs.selected_file)
+
+    createdf_parser = argparse.ArgumentParser()
+    createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier as 4-character hex string')
+    createdf_parser._action_groups.pop()
+    createdf_required = createdf_parser.add_argument_group('required arguments')
+    createdf_optional = createdf_parser.add_argument_group('optional arguments')
+    createdf_sja_optional = createdf_parser.add_argument_group('sysmoISIM-SJA optional arguments')
+    createdf_required.add_argument('--ef-arr-file-id', required=True, type=str, help='Referenced Security: File Identifier of EF.ARR')
+    createdf_required.add_argument('--ef-arr-record-nr', required=True, type=int, help='Referenced Security: Record Number within EF.ARR')
+    createdf_optional.add_argument('--shareable', action='store_true', help='Should the file be shareable?')
+    createdf_optional.add_argument('--aid', type=str, help='Application ID (creates an ADF, instead of a DF)')
+    # mandatory by spec, but ignored by several OS, so don't force the user
+    createdf_optional.add_argument('--total-file-size', type=int, help='Physical memory allocated for DF/ADi in octets')
+    createdf_sja_optional.add_argument('--permit-rfm-create', action='store_true')
+    createdf_sja_optional.add_argument('--permit-rfm-delete-terminate', action='store_true')
+    createdf_sja_optional.add_argument('--permit-other-applet-create', action='store_true')
+    createdf_sja_optional.add_argument('--permit-other-applet-delete-terminate', action='store_true')
+
+    @cmd2.with_argparser(createdf_parser)
+    def do_create_df(self, opts):
+        """Create a new DF below the currently selected DF.  Requires related privileges."""
+        file_descriptor = {
+            'file_descriptor_byte': {
+                'shareable': opts.shareable,
+                'file_type': 'df',
+                'structure': 'no_info_given',
+            }
+        }
+        ies = []
+        ies.append(FileDescriptor(decoded=file_descriptor))
+        ies.append(FileIdentifier(decoded=opts.FILE_ID))
+        if opts.aid:
+            ies.append(DfName(decoded=opts.aid))
+        ies.append(LifeCycleStatusInteger(decoded='operational_activated'))
+        ies.append(SecurityAttribReferenced(decoded={'ef_arr_file_id': opts.ef_arr_file_id,
+                                                     'ef_arr_record_nr': opts.ef_arr_record_nr }))
+        if opts.total_file_size:
+            ies.append(TotalFileSize(decoded=opts.total_file_size))
+        # TODO: Spec states PIN Status Template DO is mandatory
+        if opts.permit_rfm_create or opts.permit_rfm_delete_terminate or opts.permit_other_applet_create or opts.permit_other_applet_delete_terminate:
+               toolkit_ac = {
+                   'rfm_create': opts.permit_rfm_create,
+                   'rfm_delete_terminate': opts.permit_rfm_delete_terminate,
+                   'other_applet_create': opts.permit_other_applet_create,
+                   'other_applet_delete_terminate': opts.permit_other_applet_delete_terminate,
+                   }
+               ies.append(ProprietaryInformation(children=[ToolkitAccessConditions(decoded=toolkit_ac)]))
+        fcp = FcpTemplate(children=ies)
+        (data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
+        # the newly-created file is automatically selected but our runtime state knows nothing of it
+        self._cmd.rs.select_file(self._cmd.rs.selected_file)