# -*- coding: UTF-8 -*-
"""
card: Library adapted to request (U)SIM cards and other types of telco cards.
Copyright (C) 2010 Benoit Michau

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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""

#################################
# Python library to work on
# SIM card
# communication based on ISO7816 card
#
# needs pyscard from:
# http://pyscard.sourceforge.net/
#################################

from card.ICC import ISO7816
from card.FS import SIM_FS, MF_FS
from card.utils import *

SIM_service_table = {
    1 : "CHV1 disable function",
    2 : "Abbreviated Dialling Numbers (ADN)",
    3  : "Fixed Dialling Numbers (FDN)",
    4  : "Short Message Storage (SMS)",
    5  : "Advice of Charge (AoC)",
    6  : "Capability Configuration Parameters (CCP)",
    7  : "PLMN selector",
    8  : "RFU",
    9  : "MSISDN",
    10 : "Extension1",
    11 : "Extension2",
    12 : "SMS Parameters",
    13 : "Last Number Dialled (LND)",
    14 : "Cell Broadcast Message Identifier",
    15 : "Group Identifier Level 1",
    16 : "Group Identifier Level 2",
    17 : "Service Provider Name",
    18 : "Service Dialling Numbers (SDN)",
    19 : "Extension3",
    20 : "RFU",
    21 : "VGCS Group Identifier List (EFVGCS and EFVGCSS)",
    22 : "VBS Group Identifier List (EFVBS and EFVBSS)",
    23 : "enhanced Multi-Level Precedence and Pre-emption Service",
    24 : "Automatic Answer for eMLPP",
    25 : "Data download via SMS-CB",
    26 : "Data download via SMS-PP",
    27 : "Menu selection",
    28 : "Call control",
    29 : "Proactive SIM",
    30 : "Cell Broadcast Message Identifier Ranges",
    31 : "Barred Dialling Numbers (BDN)",
    32 : "Extension4",
    33 : "De-personalization Control Keys",
    34 : "Co-operative Network List",
    35 : "Short Message Status Reports",
    36 : "Network's indication of alerting in the MS ",
    37 : "Mobile Originated Short Message control by SIM ",
    38 : "GPRS",
    39 : "Image (IMG)",
    40 : "SoLSA (Support of Local Service Area)",
    41 : "USSD string data object supported in Call Control",
    42 : "RUN AT COMMAND command",
    43 : "User controlled PLMN Selector with Access Technology",
    44 : "Operator controlled PLMN Selector with Access Technology",
    45 : "HPLMN Selector with Access Technology",
    46 : "CPBCCH Information",
    47 : "Investigation Scan",
    48 : "Extended Capability Configuration Parameters",
    49 : "MExE",
    50 : "RPLMN last used Access Technology",
    51 : "PLMN Network Name",
    52 : "Operator PLMN List",
    53 : "Mailbox Dialling Numbers ",
    54 : "Message Waiting Indication Status",
    55 : "Call Forwarding Indication Status",
    56 : "Service Provider Display Information",
    }


class SIM(ISO7816):
    """
    define attributes, methods and facilities for ETSI / 3GPP SIM card
    check SIM specifications in ETSI TS 102.221 and 3GPP TS 51.011
    
    inherit methods and objects from ISO7816 class
    use self.dbg = 1 or more to print live debugging information
    """
    
    def __init__(self, atr = None):
        """
        initialize like an ISO7816-4 card with CLA=0xA0
        can also be used for USIM working in SIM mode,
        """
        ISO7816.__init__(self, atr, CLA=0xA0)
        
        if self.dbg >= 2:
            log(3, '(SIM.__init__) type definition: %s' % type(self))
            log(3, '(SIM.__init__) CLA definition: %s' % hex(self.CLA))
        
    @staticmethod
    def sw_status(sw1, sw2):
        """
        sw_status(sw1=int, sw2=int) -> string
        
        extends SW status bytes interpretation from ISO7816 
        with ETSI / 3GPP SW codes
        helps to speak with the smartcard!
        """
        status = ISO7816.sw_status(sw1, sw2)
        if sw1 == 0x91: status = 'normal processing, with extra info ' \
            'containing a command for the terminal: length of the ' \
            'response data %d' % sw2
        elif sw1 == 0x9E: status = 'normal processing, SIM data download ' \
            'error: length of the response data %d' % sw2
        elif sw1 == 0x9F: status = 'normal processing: length of the ' \
            'response data %d' % sw2
        elif (sw1, sw2) == (0x93, 0x00): status = 'SIM application toolkit ' \
            'busy, command cannot be executed at present'
        elif sw1 == 0x92 :
            status = 'memory management'
            if sw2 < 16: status += ': command successful but after %d '\
                'retry routine' % sw2
            elif sw2 == 0x40: status += ': memory problem'
        elif sw1 == 0x94:
            status = 'referencing management'
            if sw2 == 0x00: status += ': no EF selected'
            elif sw2 == 0x02: status += ': out of range (invalid address)'
            elif sw2 == 0x04: status += ': file ID or pattern not found'
            elif sw2 == 0x08: status += ': file inconsistent with the command'
        elif sw1 == 0x98:
            status = 'security management'
            if sw2 == 0x02: status += ': no CHV initialized'
            elif sw2 == 0x04: status += ': access condition not fulfilled, ' \
                'at least 1 attempt left'
            elif sw2 == 0x08: status += ': in contradiction with CHV status'
            elif sw2 == 0x10: status += ': in contradiction with ' \
                'invalidation status'
            elif sw2 == 0x40: status += ': unsuccessful CHV verification, ' \
                'no attempt left'
            elif sw2 == 0x50: status += ': increase cannot be performed, ' \
                'max value reached'
            elif sw2 == 0x62: status += ': authentication error, ' \
                'application specific'
            elif sw2 == 0x63: status += ': security session expired'
        return status
    
    def verify_pin(self, pin='', pin_type=1):
        """
        verify CHV1 (PIN code) or CHV2 with VERIFY APDU command
        call ISO7816 VERIFY method
        """
        if pin_type in [1, 2] and type(pin) is str and \
        len(pin) == 4 and 0 <= int(pin) < 10000:
            PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF]
            self.coms.push( self.VERIFY(P2=pin_type, Data=PIN) )
        else: 
            if self.dbg: 
                log(2, '(verify_pin) bad input parameters')
    
    def disable_pin(self, pin='', pin_type=1):
        """
        disable CHV1 (PIN code) or CHV2 with DISABLE_CHV APDU command
        TIP: do it as soon as you can when you are working 
        with a SIM / USIM card for which you know the PIN!
        call ISO7816 DISABLE method
        """
        if pin_type in [1, 2] and type(pin) is str and \
        len(pin) == 4 and 0 <= int(pin) < 10000:
            PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF]
            self.coms.push( self.DISABLE_CHV(P2=pin_type, Data=PIN) )
        else:
            if self.dbg: 
                log(2, '(disable_pin) bad input parameters')
    
    def enable_pin(self, pin='', pin_type=1):
        """
        enable CHV1 (PIN code) or CHV2 with ENABLE_CHV APDU command
        call ISO7816 ENABLE method
        """
        if pin_type in [1, 2] and type(pin) is str and \
        len(pin) == 4 and 0 <= int(pin) < 10000:
            PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF]
            self.coms.push( self.ENABLE_CHV(P2=pin_type, Data=PIN) )
        else:
            if self.dbg: 
                log(2, '(enable_pin) bad input parameters')
    
    def unblock_pin(self, pin_type=1, unblock_pin=''):
        """
        WARNING: not correctly implemented!!!
            and PUK are in general 8 nums...
        TODO: make it correctly!
        APDU Tx de-activated

        unblock CHV1 (PIN code) or CHV2 with UNBLOCK_CHV APDU command 
        and set 0000 value for new PIN
        call ISO7816 UNBLOCK_CHV method
        """
        log(1, '(unblock_pin) not implemented: aborting')
        return
        #if pin_type == 1: 
        #    pin_type = 0
        if pin_type in [0, 2] and type(unblock_pin) is str and \
        len(unblock_pin) == 4 and 0 <= int(unblock_pin) < 10000:
            UNBL_PIN = [ord(i) for i in unblock_pin] + [0xFF, 0xFF, 0xFF, 0xFF]
            #self.coms.push( self.UNBLOCK_CHV(P2=pin_type, Lc=0x10, \
            #                Data=UNBL_PIN + \
            #                [0x30, 0x30, 0x30, 0x30, 0xFF, 0xFF, 0xFF, 0xFF]) )
        else:
            if self.dbg: 
                log(2, '(unblock_pin) bad input parameters')
            #return self.UNBLOCK_CHV(P2=pin_type)
    
    def parse_file(self, Data=[]):
        """
        parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file)
        
        parses a list of bytes returned when selecting a file
        interprets the content of some informative bytes for right accesses, 
        type / format of file... see TS 51.011
        works over the SIM file structure
        """
        fil = {}
        fil['Size'] = Data[2]*0x100 + Data[3]
        fil['File Identifier'] = Data[4:6]
        fil['Type'] = ('RFU', 'MF', 'DF', '', 'EF')[Data[6]]
        fil['Length'] = Data[12]
        if fil['Type'] == 'MF' or fil['Type'] == 'DF':
            fil['DF_num'] = Data[14]
            fil['EF_num'] = Data[15]
            fil['codes_num'] = Data[16]
            fil['CHV1'] = ('not initialized','initialized')\
                          [Data[18] >> 7]\
                        + ': %d attempts remain' % (Data[18] & 0x0F)
            fil['unblock_CHV1'] = ('not initialized','initialized')\
                                  [Data[19] >> 7]\
                                + ': %d attempts remain' % (Data[19] & 0x0F)
            fil['CHV2'] = ('not initialized','initialized')\
                          [Data[20] >> 7]\
                        + ': %d attempts remain' % (Data[20] & 0x0F)
            fil['unblock_CHV2'] = ('not initialized','initialized')\
                                  [Data[21] >> 7]\
                                + ': %d attempts remain' % (Data[21] & 0x0F)
            if len(Data) > 23: 
                fil['Adm'] = Data[23:]
        elif fil['Type'] == 'EF':
            cond = ('ALW', 'CHV1', 'CHV2', 'RFU', 'ADM_4', 'ADM_5', 
                    'ADM_6', 'ADM_7', 'ADM_8', 'ADM_9', 'ADM_A',
                    'ADM_B', 'ADM_C', 'ADM_D', 'ADM_E', 'NEW')
            fil['UPDATE'] = cond[Data[8] & 0x0F]
            fil['READ'] = cond[Data[8] >> 4]
            fil['INCREASE'] = cond[Data[9] >> 4]
            fil['INVALIDATE'] = cond[Data[10] & 0x0F]
            fil['REHABILITATE'] = cond[Data[10] >> 4]
            fil['Status'] = ('not read/updatable when invalidated', 
                              'read/updatable when invalidated')\
                            [byteToBit(Data[11])[5]] \
                          + (': invalidated',': not invalidated')\
                            [byteToBit(Data[11])[7]]
            fil['Structure'] = ('transparent', 'linear fixed', '', 'cyclic')\
                               [Data[13]]
            if fil['Structure'] == 'cyclic': 
                fil['INCREASE'] = byteToBit(Data[7])[1]
            if len(Data) > 14: 
                fil['Record Length'] = Data[14]
        return fil
    
    def run_gsm_alg(self, RAND=16*[0x00]):
        """
        self.run_gsm_alg( RAND ) -> ( SRES, Kc )
            RAND : list of bytes, length 16
            SRES : list of bytes, length 4
            Kc : list of bytes, length 8
            
        runs GSM authentication algorithm: 
            accepts any kind of RAND (old GSM fashion)
        feed with RAND 16 bytes value
        returns a list with SRES and Kc, or None on error
        """
        if len(RAND) != 16:
            if self.dbg: 
                log(1, '(run_gsm_alg) bad RAND value: aborting')
            return None
        # select DF_GSM directory
        self.select([0x7F, 0x20])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(run_gsm_alg) %s' % self.coms())
            return None
        # run authentication
        self.coms.push(self.INTERNAL_AUTHENTICATE(P1=0x00, P2=0x00, Data=RAND))
        if self.coms()[2][0] != 0x9F:
            if self.dbg >= 2: 
                log(3, '(run_gsm_alg) %s' % self.coms())
            return None
        # get authentication response
        self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1]))
        if self.coms()[2] != (0x90, 0x00):
            if self.dbg >= 2: 
                log(3, '(run_gsm_alg) %s' % self.coms())
            return None
        SRES, Kc = self.coms()[3][0:4], self.coms()[3][4:]
        return [ SRES, Kc ]
    
    def get_imsi(self):
        """
        self.get_imsi() -> string(IMSI)
        
        reads IMSI value at address [0x6F, 0x07]
        returns IMSI string on success or None on error
        """
        # select DF_GSM for SIM card
        self.select([0x7F, 0x20])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_imsi) %s' % self.coms())
            return None
        
        # select IMSI file
        imsi = self.select([0x6F, 0x07])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_imsi) %s' % self.coms())
            return None
        
        # and parse the received data into the IMSI structure
        if 'Data' in imsi.keys() and len(imsi['Data']) == 9:
            return decode_BCD(imsi['Data'])[3:]
        
        # if issue with the content of the DF_IMSI file
        if self.dbg >= 2: 
            log(3, '(get_imsi) %s' % self.coms())
        return None
    
    def get_services(self):
        """
        self.get_services() -> None
        
        reads SIM Service Table at address [0x6F, 0x38]
        returns list of services allowed / activated
        """
        # select DF_GSM for SIM card
        self.select([0x7F, 0x20])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_services) %s' % self.coms())
            return None
        
        # select SST file
        sst = self.select([0x6F, 0x38])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_services) %s' % self.coms())
            return None
        
        # parse data and prints corresponding services
        if 'Data' in sst.keys() and len(sst['Data']) >= 2:
            return self.get_services_from_sst(sst['Data'])
    
    def read_services(self):
        """
        self.read_services() -> None
        
        reads SIM Service Table at address [0x6F, 0x38]
        prints services allowed / activated
        returns None
        """
        serv = self.get_services()
        for s in serv:
            print(s)
    
    def get_services_from_sst(self, sst=[0, 0]):
        services = []
        cnt = 0
        for B in sst:
            # 2 bits per service -> 4 services per byte
            for i in range(0, 7, 2):
                cnt += 1
                if B & 2**i:
                    info = 'allocated'
                    if B & (2**i+1):
                        info += ' | activated'
                    if cnt in SIM_service_table:
                        services.append('%i : %s : %s' \
                                    % (cnt, SIM_service_table[cnt], info))
                    else:
                        services.append('%i : %s' % (cnt, info))
        return services
    
    def explore_fs(self, filename='sim_fs', depth=True, emul=False):
        """
        self.explore_fs(self, filename='sim_fs') -> void
            filename: file to write in information found
            depth: depth in recursivity, True=infinite
        
        brute force all file addresses from MF recursively 
        (until no more DF are found)
        write information on existing DF and file in the output file
        """
        simfs_entries = MF_FS.keys()
        if not emul:
            self.explore_DF([], None, depth)
        
        fd = open(filename, 'w')
        fd.write('\n### MF ###\n')
        f = self.select()
        write_dict(f, fd)
        fd.write('\n')
        #
        for f in self.FS:
            path = tuple(f['Absolut Path'])
            if path in simfs_entries:
                f['Name'] = MF_FS[path]
            write_dict(f, fd)
            fd.write('\n')
        
        fd.close()
    
    def get_ICCID(self):
        # select MF
        self.select([0x3F, 0x0])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_ICCID) %s' % self.coms())
            return None
        
        # select IMSI file
        iccid = self.select([0x2F, 0xE2])
        if self.coms()[2] != (0x90, 0x00): 
            if self.dbg >= 2: 
                log(3, '(get_ICCID) %s' % self.coms())
            return None
        
        # and parse the received data into the IMSI structure
        if 'Data' in iccid.keys() and len(iccid['Data']) >= 10:
            return decode_BCD(iccid['Data'])
        
        # if issue with the content of the ICCID file
        if self.dbg >= 2: 
            log(3, '(get_ICCID) %s' % self.coms())
        return None


