# -*- 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 # smartcard defined with ISO 7816 # # Specially designed SIM and USIM class # for ETSI / 3GPP cards # # needs pyscard from: # http://pyscard.sourceforge.net/ ################################# # classic python modules import os import re # smartcard python modules from pyscard from smartcard.CardType import AnyCardType from smartcard.CardType import ATRCardType from smartcard.CardRequest import CardRequest from smartcard.CardConnection import CardConnection from smartcard.ATR import ATR from smartcard.Exceptions import CardConnectionException from smartcard.util import toHexString from card.utils import * ########################################################### # ISO7816 class with attributes and methods as defined # by ISO-7816 part 4 standard for smartcard ########################################################### class ISO7816(object): """ define attributes, methods and facilities for ISO-7816-4 standard smartcard use self.dbg = 1 or more to print live debugging information standard instructions codes available in "INS_dic" class dictionnary standard file tags available in "file_tags" class dictionnary """ dbg = 0 INS_dic = { 0x04 : 'DEACTIVATE FILE', 0x0C : 'ERASE RECORD(S)', 0x0E : 'ERASE BINARY', 0x0F : 'ERASE BINARY', 0x10 : 'TERMINAL PROFILE', 0x12 : 'FETCH', 0x14 : 'TERMINAL RESPONSE', 0x20 : 'VERIFY', 0x21 : 'VERIFY', 0x22 : 'MANAGE SECURITY ENVIRONMENT', 0x24 : 'CHANGE PIN', 0x26 : 'DISABLE PIN', 0x28 : 'ENABLE PIN', 0x2A : 'PERFORM SECURITY OPERATION', 0x2C : 'UNBLOCK PIN', 0x32 : 'INCREASE', 0x44 : 'ACTIVATE FILE', 0x46 : 'GENERATE ASYMETRIC KEY PAIR', 0x70 : 'MANAGE CHANNEL', 0x73 : 'MANAGE SECURE CHANNEL', 0x75 : 'TRANSACT DATA', 0x82 : 'EXTERNAL AUTHENTICATE', 0x84 : 'GET CHALLENGE', 0x86 : 'GENERAL AUTHENTICATE', 0x87 : 'GENERAL AUTHENTICATE', 0x88 : 'INTERNAL AUTHENTICATE', 0x89 : 'AUTHENTICATE', 0x99 : 'PROGRAM SYSMO_USIM', 0xA0 : 'SEARCH BINARY', 0xA1 : 'SEARCH BINARY', 0xA2 : 'SEARCH RECORD', 0xA4 : 'SELECT FILE', 0xA8 : 'GET PROCESSING OPTIONS', 0xAA : 'TERMINAL CAPABILITY', 0xB0 : 'READ BINARY', 0xB1 : 'READ BINARY', 0xB2 : 'READ RECORD(S)', 0xB3 : 'READ RECORD(S)', 0xC0 : 'GET RESPONSE', 0xC2 : 'ENVELOPE', 0xC3 : 'ENVELOPE', 0xCA : 'GET DATA', 0xCB : 'RETRIEVE DATA', 0xD2 : 'WRITE RECORD', 0xD6 : 'UPDATE BINARY', 0xD7 : 'UPDATE BINARY', 0xDA : 'SET DATA', 0xDB : 'SET DATA', 0xDC : 'UPDATE RECORD', 0xDD : 'UPDATE RECORD', 0xE0 : 'CREATE FILE', 0xE2 : 'APPEND RECORD', 0xE4 : 'DELETE FILE', 0xE6 : 'TERMINATE DF', 0xE8 : 'TERMINATE EF', 0xF2 : 'STATUS', 0xFE : 'TERMINATE CARD USAGE', } file_tags = { 0x80 : 'Size', 0x81 : 'Length', 0x82 : 'File Descriptor', 0x83 : 'File Identifier', 0x84 : 'DF Name', 0x85 : 'Proprietary no-BERTLV', 0x86 : 'Proprietary Security Attribute', 0x87 : 'EF with FCI extension', 0x88 : 'Short File Identifier', 0x8A : 'Life Cycle Status', 0x8B : 'Security Attributes ref to expanded', 0x8C : 'Security Attributes compact', 0x8D : 'EF with Security Environment', 0x8E : 'Channel Security Attribute', 0xA0 : 'Security Attribute for DO', 0xA1 : 'Proprietary Security Attribute', 0xA2 : 'DO Pairs', 0xA5 : 'Proprietary BERTLV', 0xAB : 'Security Attribute expanded', } def __init__(self, atr=None, CLA=0x00): """ connect smartcard and defines class CLA code for communication uses "pyscard" library services creates self.CLA attribute with CLA code and self.coms attribute with associated "apdu_stack" instance """ if (atr): cardtype = ATRCardType(atr) else: cardtype = AnyCardType() cardrequest = CardRequest(timeout=1, cardType=cardtype) self.cardservice = cardrequest.waitforcard() self.cardservice.connection.connect() self.reader = self.cardservice.connection.getReader() self.ATR = self.cardservice.connection.getATR() self.CLA = CLA self.coms = apdu_stack() def disconnect(self): """ disconnect smartcard: stops the session uses "pyscard" library service """ self.cardservice.connection.disconnect() def define_class(self, CLA=0x00): """ define smartcard class attribute for APDU command override CLA value defined in class initialization """ self.CLA = CLA def ATR_scan(self, smlist_file="/usr/share/pcsc/smartcard_list.txt"): """ print smartcard info retrieved from AnswerToReset thanks to pyscard routine if pcsc_scan is installed, use the signature file passed as argument for guessing the card """ print('\nsmartcard reader: %s' % self.reader) if self.ATR != None: print('\nsmart card ATR is: %s' % toHexString(self.ATR)) print('ATR analysis: ') print('%s' % ATR(self.ATR).dump()) print('\nhistorical bytes: %s' \ % toHexString(ATR(self.ATR).getHistoricalBytes())) ATRcs = ATR(self.ATR).getChecksum() if ATRcs : print('checksum: 0x%X' % ATRcs) else: print('no ATR checksum') print('\nusing pcsc_scan ATR list file: %s' % smlist_file) if os.path.exists(smlist_file): smlist = open(smlist_file).readlines() ATRre = re.compile('(^3[BF]){1}.{1,}$') ATRfinger = '' j = 1 for i in range(len(smlist)): if ATRre.match(smlist[i]): if re.compile(smlist[i][:len(smlist[i])-1]).\ match(toHexString(self.ATR)): while re.compile('\t.{1,}').match(smlist[i+j]): ATRfinger += smlist[i+j][1:] j += j if ATRfinger == '' : print('no ATR fingerprint found in file: %s' % smlist_file) else: print('smartcard ATR fingerprint:\n%s' % ATRfinger) else: print('%s file not found' % smlist_file) @staticmethod def sw_status(sw1, sw2): """ sw_status(sw1=int, sw2=int) -> string SW status bytes interpretation as defined in ISO-7816 part 4 standard helps to speak and understand with the smartcard! """ status = 'undefined status' if sw1 == 0x90 and sw2 == 0x00: status = 'normal processing: ' \ 'command accepted: no further qualification' elif sw1 == 0x61: status = 'normal processing: %i bytes ' \ 'still available' % sw2 elif sw1 == 0x62: status = 'warning processing: state of non-volatile '\ 'memory unchanged' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x81: status += ': part of returned data may' \ 'be corrupted' elif sw2 == 0x82: status += ': end of file/record reached ' \ 'before reading Le bytes' elif sw2 == 0x83: status += ': selected file invalidated' elif sw2 == 0x84: status += ': FCI not formatted' elif sw2 == 0x85: status += ': selected file in termination state' elif sw2 == 0x86: status += ': no input data available ' \ 'from a sensor on the card' elif 0x01 < sw2 < 0x81: status += ': card has %s bytes pending' \ % toHexString([sw2])[1] else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x63: status = 'warning processing: state of non-volatile memory changed' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x81: status += ': file filled up by the last write' elif 0xC0 <= sw2 <= 0xCF: status += ': counter provided by %s' \ % toHexString([sw2])[1] else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x64: status = 'execution error: state of non-volatile memory unchanged' if sw2 == 0x01: status += ': immediate response expected ' \ 'by the card' elif 0x01 < sw2 < 0x81: status += ': command aborted ' \ 'by the card, recovery of %s bytes is needed' \ % toHexString([sw2]) else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x65: status = 'execution error: state of non-volatile memory changed' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x81: status += ': memory failure' else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x66: status = 'execution error: reserved for ' \ 'security-related issues' elif sw1 == 0x67 and sw2 == 0x00: status = 'checking error: ' \ 'wrong length (P3 parameter)' elif sw1 == 0x68: status = 'checking error: functions in CLA not supported' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x81: status += ': logical channel not supported' elif sw2 == 0x82: status += ': secure messaging not supported' elif sw2 == 0x83: status += ': last command of the chain expected' elif sw2 == 0x84: status += ': command chaining not supported' else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x69: status = 'checking error: command not allowed' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x81: status += ': command incompatible with ' \ 'file structure' elif sw2 == 0x82: status += ': security status not satisfied' elif sw2 == 0x83: status += ': authentication method blocked' elif sw2 == 0x84: status += ': referenced data invalidated' elif sw2 == 0x85: status += ': conditions of use not satisfied' elif sw2 == 0x86: status += ': command not allowed (no current EF)' elif sw2 == 0x87: status += ': expected SM data objects missing' elif sw2 == 0x88: status += ': SM data objects incorrect' else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x6A: status = 'checking error: wrong parameter(s) P1-P2' if sw2 == 0x00: status += ': no information given' elif sw2 == 0x80: status += ': incorrect parameters ' \ 'in the data field' elif sw2 == 0x81: status += ': function not supported' elif sw2 == 0x82: status += ': file not found' elif sw2 == 0x83: status += ': record not found' elif sw2 == 0x84: status += ': not enough memory space in the file' elif sw2 == 0x85: status += ': Lc inconsistent with TLV structure' elif sw2 == 0x86: status += ': incorrect parameters P1-P2' elif sw2 == 0x87: status += ': Lc inconsistent with P1-P2' elif sw2 == 0x88: status += ': referenced data not found' elif sw2 == 0x89: status += ': file already exists' elif sw2 == 0x8A: status += ': DF name already exists' else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) elif sw1 == 0x6B and sw2 == 0x00: status = 'checking error: '\ 'wrong parameter(s) P1-P2' elif sw1 == 0x6C: status = 'checking error: wrong length Le: ' \ 'exact length is %s' % toHexString([sw2]) elif sw1 == 0x6D and sw2 == 0x00: status = 'checking error: ' \ 'instruction code not supported or invalid' elif sw1 == 0x6E and sw2 == 0x00: status = 'checking error: ' \ 'class not supported' elif sw1 == 0x6F and sw2 == 0x00: status = 'checking error: ' \ 'no precise diagnosis' return status def sr_apdu(self, apdu, force=False): """ sr_apdu(apdu=[0x.., 0x.., ...]) -> list [ string(apdu sent information), string(SW codes interpretation), 2-tuple(sw1, sw2), list(response bytes) ] generic function to send apdu, receive and interpret response force: force card reconnection if pyscard transmission fails """ if force: try: data, sw1, sw2 = self.cardservice.connection.transmit(apdu) except CardConnectionException: ISO7816.__init__(self, CLA = self.CLA) data, sw1, sw2 = self.cardservice.connection.transmit(apdu) else: data, sw1, sw2 = self.cardservice.connection.transmit(apdu) # replaces INS code by strings when available if apdu[1] in self.INS_dic.keys(): apdu_name = self.INS_dic[apdu[1]] + ' ' else: apdu_name = '' sw_stat = self.sw_status(sw1, sw2) return ['%sapdu: %s' % (apdu_name, toHexString(apdu)), 'sw1, sw2: %s - %s' % ( toHexString([sw1, sw2]), sw_stat ), (sw1, sw2), data ] def bf_cla(self, start=0, param=[0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00]): """ bf_cla( start=int(starting CLA), param=list(bytes for selecting file 0x3F, 0x00) ) -> list( CLA which could be supported ) tries all classes CLA codes to check the possibly supported ones prints CLA suspected to be supported returns the list of those CLA codes WARNING: can block the card definitively Do not do it with your own VISA / MASTERCARD """ clist = [] for i in range(start, 256): ret = self.sr_apdu([i] + param) if ret[2] != (0x6E, 0x00): # DBG log log(3, '(CLA bruteforce) %s' % ret) clist.append(i) return clist def bf_ins(self, start=0): """ bf_cla( start=int(starting INS) ) -> list( INS which could be supported ) tries all instructions INS codes to check the supported ones prints INS suspected to be supported returns the list of those INS codes WARNING: can block the card definitively Do not do it with your own VISA / MASTERCARD """ ilist = [] for i in range(start, 256): if self.dbg >= 3: log(3, '(INS bruteforce) testing %d for INS code ' \ 'with %d CLA code' % (i, self.CLA)) ret = self.sr_apdu([self.CLA, i, 0x00, 0x00]) if ret[2] != (0x6D, 0x00): # DBG log log(3, '(INS bruteforce) %s' % ret) ilist.append(i) return ilist ### # Below is defined a list of standard commands to be used with (U)SIM cards # They are mainly defined and described in # ISO 7816 and described further in ETSI 101.221 ### def READ_BINARY(self, P1=0x00, P2=0x00, Le=0x01): """ APDU command to read the content of EF file with transparent structure Le: length of data bytes to be read call sr_apdu method """ READ_BINARY = [self.CLA, 0xB0, P1, P2, Le] return self.sr_apdu(READ_BINARY) def WRITE_BINARY(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to write the content of EF file with transparent structure Data: list of data bytes to be written call sr_apdu method """ WRITE_BINARY = [self.CLA, 0xD0, P1, P2, len(Data)] + Data return self.sr_apdu(WRITE_BINARY) def UPDATE_BINARY(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to update the content of EF file with transparent structure Data: list of data bytes to be written call sr_apdu method """ UPDATE_BINARY = [self.CLA, 0xD6, P1, P2, len(Data)] + Data return self.sr_apdu(UPDATE_BINARY) def ERASE_BINARY(self, P1=0x00, P2=0x00, Lc=None, Data=[]): """ APDU command to erase the content of EF file with transparent structure Lc: 'None' or '0x02' Data: list of data bytes to be written call sr_apdu method """ if Lc is None: ERASE_BINARY = [self.CLA, 0x0E, P1, P2] else: ERASE_BINARY = [self.CLA, 0x0E, P1, P2, 0x02] + Data return self.sr_apdu(ERASE_BINARY) def READ_RECORD(self, P1=0x00, P2=0x00, Le=0x00): """ APDU command to read the content of EF file with record structure P1: record number P2: reference control Le: length of data bytes to be read call sr_apdu method """ READ_RECORD = [self.CLA, 0xB2, P1, P2, Le] return self.sr_apdu(READ_RECORD) def WRITE_RECORD(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to write the content of EF file with record structure P1: record number P2: reference control Data: list of data bytes to be written in the record call sr_apdu method """ WRITE_RECORD = [self.CLA, 0xD2, P1, P2, len(Data)] + Data return self.sr_apdu(WRITE_RECORD) def APPEND_RECORD(self, P2=0x00, Data=[]): """ APDU command to append a record on EF file with record structure P2: reference control Data: list of data bytes to be appended on the record call sr_apdu method """ APPEND_RECORD = [self.CLA, 0xE2, 0x00, P2, len(Data)] + Data return self.sr_apdu(APPEND_RECORD) def UPDATE_RECORD(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to update the content of EF file with record structure P1: record number P2: reference control Data: list of data bytes to update the record call sr_apdu method """ APPEND_RECORD = [self.CLA, 0xDC, P1, P2, len(Data)] + Data return self.sr_apdu(APPEND_RECORD) def GET_DATA(self, P1=0x00, P2=0x00, Le=0x01): """ APDU command to retrieve data object P1 and P2: reference control for data object description Le: number of bytes expected in the response call sr_apdu method """ GET_DATA = [self.CLA, 0xCA, P1, P2, Le] return self.sr_apdu(GET_DATA) def PUT_DATA(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to store data object P1 and P2: reference control for data object description Data: list of data bytes to put in the data object structure call sr_apdu method """ if len(Data) == 0: PUT_DATA = [self.CLA, 0xDA, P1, P2] elif 1 <= len(Data) <= 255: PUT_DATA = [self.CLA, 0xDA, P1, P2, len(Data)] + Data # should never be the case, however... who wants to try else: PUT_DATA = [self.CLA, 0xDA, P1, P2, 0xFF] + Data[0:255] return self.sr_apdu(PUT_DATA) def SELECT_FILE(self, P1=0x00, P2=0x00, Data=[0x3F, 0x00], \ with_length=True): """ APDU command to select file P1 and P2: selection control Data: list of bytes describing the file identifier or address call sr_apdu method """ if with_length: Data = [min(len(Data), 255)] + Data SELECT_FILE = [self.CLA, 0xA4, P1, P2] + Data return self.sr_apdu(SELECT_FILE) def VERIFY(self, P2=0x00, Data=[]): """ APDU command to verify user PIN, password or security codes P2: reference control Data: list of bytes to be verified by the card call sr_apdu method """ if len(Data) == 0: VERIFY = [self.CLA, 0x20, 0x00, P2] elif 1 <= len(Data) <= 255: VERIFY = [self.CLA, 0x20, 0x00, P2, len(Data)] + Data # should never be the case, however... who wants to try else: VERIFY = [self.CLA, 0x20, 0x00, P2, 0xFF] + Data[0:255] return self.sr_apdu(VERIFY) def INTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to run internal authentication algorithm P1 and P2: reference control (algo, secret key selection...) Data: list of bytes containing the authentication challenge call sr_apdu method """ INTERNAL_AUTHENTICATE = [self.CLA, 0x88, P1, P2, len(Data)] + Data return self.sr_apdu(INTERNAL_AUTHENTICATE) def EXTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to conditionally update the security status of the card after getting a challenge from it P1 and P2: reference control (algo, secret key selection...) Data: list of bytes containing the challenge response call sr_apdu method """ if len(Data) == 0: EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2] elif 1 <= len(Data) <= 255: EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, len(Data)] + Data # should never be the case, however... who wants to try else: EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, 0xFF] + Data[0:255] return self.sr_apdu(EXTERNAL_AUTHENTICATE) def GET_CHALLENGE(self): """ APDU command to get a challenge for external entity authentication to the card call sr_apdu method """ GET_CHALLENGE = [self.CLA, 0x84, 0x00, 0x00] return self.sr_apdu(GET_CHALLENGE) def MANAGE_CHANNEL(self, P1=0x00, P2=0x00): """ APDU to open and close supplementary logical channels P1=0x00 to open, 0x80 to close P2=0x00, 1, 2 or 3 to ask for logical channel number call sr_apdu method """ if (P1, P2) == (0x00, 0x00): MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2, 0x01] else: MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2] return self.sr_apdu(MANAGE_CHANNEL) def GET_RESPONSE(self, Le=0x01): """ APDU command to retrieve data after selection or other kind of request that should get an extensive reply Le: expected length of data call sr_apdu method """ GET_RESPONSE = [self.CLA, 0xC0, 0x00, 0x00, Le] return self.sr_apdu(GET_RESPONSE) def ENVELOPE(self, Data=[]): """ APDU command to encapsulate data (APDU or other...) check ETSI TS 102.221 for some examples... Data: list of bytes call sr_apdu method """ if len(Data) == 0: ENVELOPE = [self.CLA, 0xC2, 0x00, 0x00] elif 1 <= len(Data) <= 255: ENVELOPE = [self.CLA, 0xC2, 0x00, 0x00, len(Data)] + Data return self.sr_apdu(ENVELOPE) def SEARCH_RECORD(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to seach pattern in the current EF file with record structure P1: record number P2: type of search Data: list of bytes describing a pattern to search for call sr_apdu method """ SEARCH_RECORD = [self.CLA, 0xA2, P1, P2, len(Data)] + Data return self.sr_apdu(SEARCH_RECORD) def DISABLE_CHV(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to disable CHV verification (such as PIN or password...) P1: let to 0x00... or read ISO and ETSI specifications P2: type of CHV to disable Data: list of bytes for CHV value call sr_apdu method """ DISABLE_CHV = [self.CLA, 0x26, P1, P2, len(Data)] + Data return self.sr_apdu(DISABLE_CHV) def ENABLE_CHV(self, P1=0x00, P2=0x00, Data=[]): """ APDU command to enable CHV verification (such as PIN or password...) P1: let to 0x00... or read ISO and ETSI specifications P2: type of CHV to enable Data: list of bytes for CHV value call sr_apdu method """ ENABLE_CHV = [self.CLA, 0x28, P1, P2, len(Data)] + Data return self.sr_apdu(ENABLE_CHV) def UNBLOCK_CHV(self, P2=0x00, Data=[]): """ APDU command to unblock CHV code (e.g. with PUK for deblocking PIN) P2: type of CHV to unblock Lc: Empty or 0x10 Data: if Lc=0x10, UNBLOCK_CHV (PUK) value and new CHV (PIN) values, each are 8 digits call sr_apdu method """ if len(Data) != 16: UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2] else: UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2, 0x10] + Data return self.sr_apdu(UNBLOCK_CHV) def FETCH(self, Le=0x01): """ APDU command to receive an ICC proactive command that will need to be responded by a TERMINAL RESPONSE Le: expected length of data call sr_apdu method """ FETCH = [self.CLA, 0x12, 0x00, 0x00, Le] return self.sr_apdu(FETCH) def TERMINAL_RESPONSE(self, Data=[]): """ APDU command to provide a response to an ICC proactive command Data: list of bytes for the response to be provided to the ICC """ TERMINAL_RESP = [self.CLA, 0x14, 0x00, 0x00, len(Data)] + Data return self.sr_apdu(TERMINAL_RESP) ########################## # evolved "macro" method for ISO7816 card # need the "coms" attribute being an apdu_stack() ########################## def parse_file(self, Data=[]): """ parse_file(self, Data) -> Dict() parses a list of bytes returned when selecting a file interprets the content of some informative bytes for file structure and parsing method... """ ber = BERTLV_parser( Data ) if self.dbg >= 3: log(3, 'BER structure:\n%s' % ber) if len(ber) > 1: # TODO: implements recursive BER object parsing log(1, '(parse_file) contain more than 1 BER object: '\ '%s\nnot implemented' % ber) # for FCP control structure, precise parsing is done # this structure seems to be the most used for (U)SIM cards if ber[0][0][2] == 0x2: fil = self.parse_FCP( ber[0][2] ) fil['Control'] = 'FCP' return fil # for FCI control structure, also trying to parse precisely # this structure is used mainly in EMV cards elif ber[0][0][2] == 0x10: fil = self.parse_FCI( ber[0][2] ) fil['Control'] = 'FCI' return fil # for other control structure, DIY fil = {} if ber[0][0][2] == 0x4: fil['Control'] = 'FMD' if self.dbg: log(1, '(parse_file) FMD file structure parsing ' \ 'not implemented') elif ber[0][0][2] == 0xF: fil['Control'] = 'FCI' if self.dbg: log(1, '(parse_file) FCI 0xF file structure parsing ' 'not implemented') else: fil['Control'] = ber[0][0] if self.dbg: log(1, '(parse_file) unknown file structure') fil['Data'] = ber[0][2] return fil def parse_FCP(self, Data=[]): """ parse_FCP(Data) -> Dict() parses a list of bytes returned when selecting a file interprets the content of some informative bytes for file structure and parsing method... """ fil = {} # loop on the Data bytes to parse TLV'style attributes toProcess = Data while len(toProcess) > 0: # TODO: seemd full compliancy # would require to work with the BERTLV parser... [T, L, V] = first_TLV_parser(toProcess) if self.dbg >= 3: if T in self.file_tags.keys(): Tag = self.file_tags[T] else: Tag = T log(3, '(parse_FCP) Tag value %s / type %s: %s' % (T, Tag, V)) # do extra processing here # File ID, DF name, Short file id if T in (0x83, 0x84, 0x88): fil[self.file_tags[T]] = V # Security Attributes compact format elif T == 0x8C: fil[self.file_tags[T]] = V fil = self.parse_compact_security_attribute(V, fil) # Security Attributes ref to expanded elif T == 0x8B: fil[self.file_tags[T]] = V fil = self.parse_expanded_security_attribute(V, fil) # other security attributes... not implemented elif T in (0x86, 0x8E, 0xA0, 0xA1, 0xAB): fil[self.file_tags[T]] = V # TODO: no concrete parsing at this time... if self.dbg: log(2, '(parse_FCP) parsing security attributes not ' \ 'implemented for tag 0x%X' % T) fil = self.parse_security_attribute(V, fil) # file size or length elif T in (0x80, 0x81): fil[self.file_tags[T]] = sum( [ V[i] * pow(0x100, len(V)-i-1) \ for i in range(len(V)) ] ) # file descriptor, deducting file access, type and structure elif T == 0x82: assert( L in (2, 5) ) fil[self.file_tags[T]] = V fil = self.parse_file_descriptor(V, fil) # life cycle status elif T == 0x8A: fil = self.parse_life_cycle(V, fil) # proprietary information elif T == 0xA5: fil = self.parse_proprietary(V, fil) else: if T in self.file_tags.keys(): fil[self.file_tags[T]] = V else: fil[T] = V # truncate the data to process and loop if L < 256: toProcess = toProcess[L+2:] else: toProcess = toProcess[L+4:] # and return the file return fil @staticmethod def parse_life_cycle(Data, fil): """ parses a list of bytes provided in Data interprets the content as the life cycle and enriches the file dictionnary passed as argument """ if Data[0] == 1: fil['Life Cycle Status'] = 'creation state' elif Data[0] == 3: fil['Life Cycle Status'] = 'initialization state' elif Data[0] in (5, 7): fil['Life Cycle Status'] = 'operational state' \ ' - activated' elif Data[0] in (4, 6): fil['Life Cycle Status'] = 'operational state' \ ' - deactivated' elif Data[0] in range(12, 15): fil['Life Cycle Status'] = \ 'termination state' elif Data[0] >= 16: fil['Life Cycle Status'] = 'proprietary' else: fil['Life Cycle Status'] = 'RFU' return fil @staticmethod def parse_file_descriptor(Data, fil): """ parses a list of bytes provided in Data interprets the content as the file descriptor and enriches the file dictionnary passed as argument """ # parse the File Descriptor Byte fd = Data[0] fd_type = (fd >> 3) & 0b00111 fd_struct = fd & 0b00000111 # get Structure, Access and Type # bit b8 if (fd >> 7) & 0b1: fil['Structure'] = 'RFU' # access bit b7 if (fd >> 6) & 0b1: fil['Access'] = 'shareable' else : fil['Access'] = 'not shareable' # structure bits b1 to b3 if fd_struct == 0: fil['Structure'] = 'no information' elif fd_struct == 1: fil['Structure'] = 'transparent' elif fd_struct == 2: fil['Structure'] = 'linear fixed' elif fd_struct == 3: fil['Structure'] = 'linear fixed TLV' elif fd_struct == 4: fil['Structure'] = 'linear variable' elif fd_struct == 5: fil['Structure'] = 'linear variable TLV' elif fd_struct == 6: fil['Structure'] = 'cyclic' elif fd_struct == 7: fil['Structure'] = 'cyclic TLV' else : fil['Structure'] = 'RFU' # type bits b4 to b6 if fd_type == 0: fil['Type'] = 'EF working' elif fd_type == 1: fil['Type'] = 'EF internal' elif fd_type == 7: fil['Type'] = 'DF' if fd_struct == 1: fil['Structure'] = 'BER-TLV' elif fd_struct == 2: fil['Structure'] = 'TLV' else: fil['Type'] = 'EF proprietary' # for linear and cyclic EF: # the following is convenient for UICC, # but looks not fully conform to ISO standard # see coding convention in ISO 7816-4 Table 87 if len(Data) == 5: fil['Record Length'], fil['Record Number'] = Data[3], Data[4] return fil @staticmethod def parse_proprietary(Data, fil): """ parses a list of bytes provided in Data interprets the content as the proprietary parameters and enriches the file dictionnary passed as argument """ propr_tags = { 0x80:"UICC characteristics", 0x81:"Application power consumption", 0x82:"Minimum application clock frequency", 0x83:"Amount of available memory", 0x84:"File details", 0x85:"Reserved file size", 0x86:"Maximum file size", 0x87:"Supported system commands", 0x88:"Specific UICC environmental conditions", } while len(Data) > 0: [T, L, V] = first_TLV_parser( Data ) if T in propr_tags.keys(): fil[propr_tags[T]] = V Data = Data[L+2:] return fil def parse_compact_security_attribute(self, Data, fil): """ parses a list of bytes provided in Data interprets the content as the compact form for security parameters and enriches the file dictionnary passed as argument """ # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format AM = Data[0] SC = Data[1:] sec = '#' # check access mode if 'Type' in fil.keys(): # DF security attributes parsing if fil['Type'] == 'DF': sec += self._DF_access_mode(AM) # EF security attributes parsing else: sec += self._EF_access_mode(AM) # loop on security conditions for the given access mode: for cond in SC: sec += self._sec_cond(cond) # return security conditions if parsed, return raw bytes otherwise if sec == '#': fil['Security Attributes raw'] = Data else: fil['Security Attributes'] = sec return fil @staticmethod def _DF_access_mode(AM): sec = '' if AM & 0b10000000 == 0: if AM & 0b01000000: sec += ' DELETE FILE #' if AM & 0b00100000: sec += ' TERMINATE DF #' if AM & 0b00010000: sec += ' ACTIVATE FILE #' if AM & 0b00001000: sec += ' DEACTIVATE FILE #' if AM & 0b00000100: sec += ' CREATE DF #' if AM & 0b00000010: sec += ' CREATE EF #' if AM & 0b00000001: sec += ' DELETE FILE #' return sec @staticmethod def _EF_access_mode(AM): sec = '' if AM & 0b10000000 == 0: if AM & 0b01000000: sec += ' DELETE FILE #' if AM & 0b00100000: sec += ' TERMINATE EF #' if AM & 0b00010000: sec += ' ACTIVATE FILE #' if AM & 0b00001000: sec += ' DEACTIVATE FILE #' if AM & 0b00000100: sec += ' WRITE / APPEND #' if AM & 0b00000010: sec += ' UPDATE / ERASE #' if AM & 0b00000001: sec += ' READ / SEARCH #' return sec @staticmethod def _Obj_access_mode(AM): sec = '' if AM & 0b00000100: sec += 'MANAGE SEC ENVIRONMENT #' if AM & 0b00000010: sec += 'PUT DATA' if AM & 0b00000001: sec += 'GET DATA' return sec @staticmethod def _Tab_access_mode(val): sec = '' if AM & 0b10000000 == 0: if AM & 0b01000000: sec += ' CREATE / DELETE USER #' if AM & 0b00100000: sec += ' GRANT / REVOKE #' if AM & 0b00010000: sec += ' CREATE TABLE / VIEW / DICT #' if AM & 0b00001000: sec += ' DROP TABLE / VIEW #' if AM & 0b00000100: sec += ' INSERT #' if AM & 0b00000010: sec += ' UPDATE / DELETE #' if AM & 0b00000001: sec += ' FETCH #' return sec @staticmethod def _sec_cond(cond): sec = '' if cond == 0 : sec += ' Always #' elif cond == 0xff: sec += ' Never #' else: sec += ' SEID %s #' % (cond & 0b00001111) if cond & 0b10000000: sec += ' all conditions #' else: sec += ' at least 1 condition #' if cond & 0b01000000: sec += ' secure messaging #' if cond & 0b00100000: sec += ' external authentication #' if cond & 0b00010000: sec += ' user authentication #' return sec @staticmethod def parse_expanded_security_attribute(Data, fil): """ check references to EF_ARR file containing access conditions see ISO 7816-4 """ # self.ARR = {ARR_id:[ARR_content],...} return fil file_length = len(Data) if file_length == 1: ARR_byte = Data elif file_length == 3: ARR_ref = Data[0:2] ARR_byte = Data[2:3] elif file_length > 3: ARR_ref = Data[0:2] # handle SEID and ARR.byte in 2 // lists SEID_bytes, ARR_bytes = [], [] # in case file_length is not even: truncate it... if file_length%2 == 1: file_length -= 1 # parse SEID / ARR.bytes for i in range(2, file_length, 2): SEID_byte.append(Data[i:i+1]) ARR_byte.append(Data[:+1:i+2]) @staticmethod def parse_security_attribute(Data, fil): """ TODO: to implement... need to work further on how to do it (with ref to EF_ARR) """ # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format # not implemented yet (looks like useless for (U)SIM card ?) return fil def parse_FCI(self, Data=[]): """ parse_FCI(Data) -> Dict() parses a list of bytes returned when selecting a file interprets the content of some informative bytes for file structure and parsing method... """ fil = {} # loop on the Data bytes to parse TLV'style attributes toProcess = Data while len(toProcess) > 0: # TODO: seemd full compliancy # would require to work with the BERTLV parser... [T, L, V] = first_TLV_parser(toProcess) if self.dbg >= 3: if T in self.file_tags.keys(): Tag = self.file_tags[T] else: Tag = T log(3, '(parse_FCI) Tag value %s / type %s: %s' % (T, Tag, V)) # application template if T == 0x61: fil['Application Template'] = V # do extra processing here # File ID, DF name, Short file id elif T in (0x83, 0x84, 0x88): fil[self.file_tags[T]] = V # Security Attributes compact format elif T == 0x8C: fil[self.file_tags[T]] = V fil = self.parse_compact_security_attribute(V, fil) # Security Attributes ref to expanded elif T == 0x8B: fil[self.file_tags[T]] = V fil = self.parse_expanded_security_attribute(V, fil) # other security attributes... not implemented elif T in (0x86, 0x8E, 0xA0, 0xA1, 0xAB): fil[self.file_tags[T]] = V # TODO: no concrete parsing at this time... if self.dbg: log(2, '(parse_FCP) parsing security attributes not ' \ 'implemented for tag 0x%X' % T) fil = self.parse_security_attribute(V, fil) # file size or length elif T in (0x80, 0x81): fil[self.file_tags[T]] = sum( [ V[i] * pow(0x100, len(V)-i-1) \ for i in range(len(V)) ] ) # file descriptor, deducting file access, type and structure elif T == 0x82: assert( L in (2, 5) ) fil[self.file_tags[T]] = V fil = self.parse_file_descriptor(V, fil) # life cycle status elif T == 0x8A: fil = self.parse_life_cycle(V, fil) # proprietary information elif T == 0xA5: fil = self.parse_proprietary(V, fil) else: if T in self.file_tags.keys(): fil[self.file_tags[T]] = V else: fil[T] = V # truncate the data to process and loop if L < 256: toProcess = toProcess[L+2:] else: toProcess = toProcess[L+4:] # and return the file return fil def read_EF(self, fil): """ interprets the content of file parameters (Structure, Size, Length...) and enriches the file dictionnary passed as argument with "Data" key and corresponding - list of bytes for EF transparent - list of list of bytes for cyclic or linear EF """ # read EF transparent data if fil['Structure'] == 'transparent': self.coms.push( self.READ_BINARY(Le=fil['Size']) ) if self.coms()[2] != (0x90, 0x00): if self.dbg >= 3: log(3, '(read_EF) %s' % self.coms()) return fil fil['Data'] = self.coms()[3] # read EF cyclic / linear all records data elif fil['Structure'] != 'transparent': fil['Data'] = [] # for record data: need to check the number of recordings # stored in the file, and iterate for each for i in range( (fil['Size'] // fil['Record Length']) ): self.coms.push( self.READ_RECORD(P1=i+1, P2=0x04, \ Le=fil['Record Length']) ) if self.coms()[2] != (0x90, 0x00): # should mean there is an issue # somewhere in the file parsing process if self.dbg >= 2: log(2, '(read_EF) error in iterating the RECORD ' \ 'parsing at iteration %s\n%s' % (i, self.coms())) return fil if self.coms()[3][1:] == len(self.coms()[3][1:]) * [255]: # record is empty, contains padding only pass else: fil['Data'].append(self.coms()[3]) # return the [Data] for transparent or # [[Record1],[Record2]...] for cyclic / linear return fil def select(self, addr=[0x3F, 0x00], type="fid", with_length=True): """ self.select(addr=[0x.., 0x..], type="fid", with_length=True) -> dict() on success, None on error selects the file at the given address if error, returns None if processing correct: gets response with info on the file if EF file: tries to read the data within the file security conditions, aka PIN/ADM codes, need to be satified returns the complete file structure and content as a dictionnary `self`.parse_file() method currently implements only FCP structure for working with USIM different types of file selection are possible (P1 parameter of the SELECT_FILE APDU): "fid": select by file id, only the direct child of current DF or parent DF or immediate children of parent DF current DF: last selected MF / DF / ADF "pmf": select by path from MF "pdf": select by path from last selected MF / DF / ADF (or relative path) "aid": select by ADF (Application) name with_length: correspond to the Lc byte preprended to the address in the SELECT_FILE APDU APDUs exchanged available thanks to the attribute `self`.coms """ # get the UICC trigger is_UICC = isinstance(self, UICC) # handle type of selection: if type == "pmf": P1 = 0x08 elif type == "pdf": P1 = 0x09 elif type == "aid": P1 = 0x04 # the default case, selection by "fid": else: P1 = 0x00 # for UICC instance # ask the return of the FCP template for the selected file: if is_UICC: P2 = 0x04 else: P2 = 0x00 # used to get back to MF without getting MF attributes: #if len(addr) == 0: # P1, P2 = 0x00, 0x0C # this is however not correct... commented # select file and check SW; if error, returns None, # else get response self.coms.push(self.SELECT_FILE(P1=P1, P2=P2, Data=addr, \ with_length=with_length)) # different SW codes for UICC and old ISO card (e.g. SIM) if is_UICC and self.coms()[2][0] != 0x61 \ or not is_UICC and self.coms()[2][0] != 0x9F: if self.dbg >= 3: log(3, '(select) %s' % self.coms()) return None # get response and check SW: # if error, return None, else parse file info self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1])) if self.coms()[2] != (0x90, 0x00): if self.dbg >= 3: log(3, '(select) %s' % self.coms()) return None data = self.coms()[3] # take the parse_file() method from the instance: # ISO7816, UICC (for USIM) or SIM file = self.parse_file(data) if 'Type' in file.keys() and file['Type'][0:2] == 'EF': file = self.read_EF(file) # finally returns the whole file dictionnary, # containing the ['Data'] key for EF file content return file ############### # The following may need some improvements ############### def go_to_path(self, path=[], under_AID=None): """ self.go_to_path(path=[0x.., 0x.., 0x.., 0x.., ..], under_AID=None) -> void selects all DF addresses successively from the path given uses the .select() method with "fid" as selection type works with AID number too """ # check path length if len(path) % 2: log(1, '(go_to_path) path length not correct: %s' % path) return # init under MF self.select([0x3F, 0x00]) # init under AID if needed if isinstance(self, UICC) and under_AID is not None: self.select_by_aid(under_AID) # select over the whole path [self.select(addr, 'fid') for addr in \ [path[i:i+2] for i in range(0,len(path),2)]] # the MF or AID directory structure is a dictionnary: # e.g. #self._MF_struct = { #tuple(df_absolute_addr) : (child_df1, child_df2, ...), #...} # or #self._AID1_struct ... # # this helps to build the blacklist: def make_blacklist(self, DF_path=[], under_AID=None): """ self.make_blacklist(DF_path=[0x.., 0x.., 0x.., 0x..], under_AID=None) -> list( DFs ) check dictionnaries describing MF or AID directory structure and return DF not to select when scanning for file ID under a DF """ # IC card Master File, never reselect it... MF = [0x3F, 0x00] # you should also avoid to reselect it # looks like an alias of the MF pseudo_MF = [0x3F, 0xFF] # init BlackList with MF and current DF BL = [ MF, pseudo_MF ] # # check if current DF is root: returns directly current_DF = DF_path[-2:] # then, AID directory structure to use if under_AID if under_AID: if not hasattr(self, '_AID%i_struct' % under_AID): if self.dbg >= 2: log(2, '(make_blacklist) AID%i directory structure not' \ ' found' % under_AID) if current_DF: BL.append(current_DF) return BL dir_struct = getattr(self, '_AID%i_struct' % under_AID) # else, select MF directory structure to use else: if not hasattr(self, '_MF_struct'): if self.dbg >= 2: log(2, '(make_blacklist) MF directory structure not found') if current_DF: BL.append(current_DF) return BL dir_struct = self._MF_struct # # if parent_DF is root (MF or AID), add only childs of root # which contains the current DF if len(DF_path) == 2: BL.extend( dir_struct[()] ) return BL # if DF is 2nd order or more, add father DF and its children # one of which contains the current DF elif len(DF_path) >= 4: BL.append( DF_path[-4:-2] ) BL.extend( dir_struct[tuple(DF_path[:-2])] ) return BL # if DF_path is empty or malformed... else: if current_DF: BL.append(current_DF) return BL def scan_DF(self, dir_path=[], under_AID=None, \ hi_addr=(0, 0xff), lo_addr=(0, 0xff)): """ self.scan_DF(dir_path=[0x.., 0x.., 0x.., 0x..], under_AID=None) -> list(filesystem), list(child_DF) try to select all file addresses under a given DF path hi_addr: 8 MSB of the file address to brute force lo_addr: 8 LSB of the file address to brute force avoid selecting blacklisted files (MF, parent_DF, brother_DF, current_DF) return list of all found files (EF, DF) and list of child DF """ # build blacklist of addresses from the current directory structure # and selected path, in order to select only child file ID: BL = self.make_blacklist(dir_path, under_AID) if self.dbg >= 2: log(3, 'blacklist: %s' % BL) # init variables to return FS, child_DF = [], [] # # init to path self.go_to_path(dir_path, under_AID) # bruteforce child file addresses i, j = 0, 0 for i in range(hi_addr[0], hi_addr[1]+1): # just make it verbose... if self.dbg and i%32 == 0: log(3, '(scan_DF) addr: %s %s' % (dir_path, [i, j])) for j in range(lo_addr[0], lo_addr[1]+1): addr = [i, j] # avoid selection of blacklisted addresses: if addr in BL: pass # select by direct file id else: file = self.select(addr, 'fid') if file: if self.dbg: log(3, '(scan_DF) found file at path: %s' \ % (dir_path + addr)) # keep track of absolute path file['Absolut Path'] = dir_path + addr # add result to grow the filesystem FS.append(file) # now fill in child_DF to potentially # grow the directory structure if 'Type' in file.keys() and file['Type'] == 'DF': # for UICC, avoid reselecting AID DF if under_AID and 'DF Name' in file.keys() \ and file['DF Name'] == self.AID[under_AID-1]: if self.dbg: log(3, '(scan_DF) USIM AID alias at %s: ' \ 'ignoring it' % addr) else: child_DF.append(addr) # replace selection to parent_path self.go_to_path(dir_path, under_AID) # # re-initialize at MF and return self.select([0x3F, 0x00]) return FS, child_DF def explore_DF(self, DF_path=[], under_AID=None, recursive=True): """ self.explore_DF(dir_path=[0x.., 0x.., 0x.., 0x..], under_AID=None, \ recursive=True) -> None try to select all file addresses under a given DF path recursively with scan_DF() method, possibly recursively (can be an `int`, to stop after a certain level) fill in self.FS dictionnary with found DF and files and self._MF_struct or self._AID`num`_struct with directory structure """ # init by scanning the given DF_path (MF or AID) FS, child_DF = self.scan_DF(DF_path, under_AID) # then init or extend self._MF_struct or # self._AID`num`_struct for blacklist management if under_AID: # if _AID`num`_struct not initialized (we are at AID root): if not hasattr(self, '_AID%i_struct' % under_AID): setattr(self, '_AID%i_struct' % under_AID, {}) # then populate _AID`num`_struct with found child_DF getattr(self, '_AID%i_struct' % under_AID)[tuple(DF_path)] = child_DF else: # if _MF_struct not initialized (we are at MF root): if not hasattr(self, '_MF_struct'): self._MF_struct = {} self._MF_struct[tuple(DF_path)] = child_DF # populate the self.FS if not hasattr(self, 'FS'): self.init_FS() self.FS.extend(FS) # # and loop to scan recursively over child_DF if recursive: # manage maximum recursion level: do not scan children DF # if absolut path is over recursion level if type(recursive) == int and len(DF_path)/2 >= recursive: return # scan children DF for path in map(DF_path.__add__, child_DF): print('recursive selection of path %s' % path) self.explore_DF(path, under_AID, recursive) def init_FS(self): self.FS = [] ############################################## # UICC is defined in ETSI 102.221 mainly, # and used for many telco applications ############################################## class UICC(ISO7816): """ define attributes, methods and facilities for ETSI UICC card check UICC specifications mainly in ETSI TS 102.221 inherits (eventually overrides) methods and objects from ISO7816 class use self.dbg = 1 or more to print live debugging information """ AID_RID = { (0xA0, 0x00, 0x00, 0x00, 0x09): 'ETSI', (0xA0, 0x00, 0x00, 0x00, 0x87): '3GPP', (0xA0, 0x00, 0x00, 0x03, 0x43): '3GPP2', (0xA0, 0x00, 0x00, 0x06, 0x45): 'OneM2M', (0xA0, 0x00, 0x00, 0x04, 0x12): 'OMA', (0xA0, 0x00, 0x00, 0x04, 0x24): 'WiMAX', (0xA0, 0x00, 0x00, 0x00, 0x03): 'GlobalPlatform', (0xA0, 0x00, 0x00, 0x01, 0x51): 'GlobalPlatform' } AID_ETSI_app_code = { (0x00, 0x00): 'Reserved', (0x00, 0x01): 'GSM', (0x00, 0x02): 'GSM SIM Toolkit', (0x00, 0x03): 'GSM SIM API for JavaCard', (0x00, 0x04): 'Tetra', (0x00, 0x05): 'UICC API for JavaCard', (0x01, 0x01): 'DVB CBMS KMS', } AID_3GPP_app_code = { (0x10, 0x01): 'UICC', (0x10, 0x02): 'USIM', (0x10, 0x03): 'USIM Toolkit', (0x10, 0x04): 'ISIM', (0x10, 0x05): 'USIM API for JavaCard', (0x10, 0x06): 'ISIM API for JavaCard', (0x10, 0x07): 'Contact Manager API for JavaCard', (0x10, 0x08): '3GPP USIM-INI', (0x10, 0x09): '3GPP USIM-RN', (0x10, 0x0A): '3GPP HPSIM', } # TODO: check USIM specific AID as defined in TS 31.130, annex C AID_3GPP2_app_code = { (0x10, 0x02): 'CSIM', } AID_OneM2M_app_code = { (0x10, 0x01): 'oneM2M UICC', (0x10, 0x02): 'oneM2M 1M2MSM', } AID_country_code = { (0xFF, 0x33): 'France', (0xFF, 0x44): 'United Kingdom', (0xFF, 0x49): 'Germany', } AID_GP_code = { (0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00): 'GlobalPlatform card manager (before v211)', (0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00): 'GlobalPlatform card manager (before v211)', (0xA0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00): 'GlobalPlatform card manager (v211 and after)', (0xA0, 0x00, 0x00, 0x00, 0x18, 0x43, 0x4D, 0x00): 'GlobalPlatform card manager (GemXpresso Pro)' } # TODO: check UICC access control AID as defined in the Android API #https://source.android.com/devices/tech/config/uicc pin_status = { 0x01 : "PIN Appl 1", 0x02 : "PIN Appl 2", 0x03 : "PIN Appl 3", 0x04 : "PIN Appl 4", 0x05 : "PIN Appl 5", 0x06 : "PIN Appl 6", 0x07 : "PIN Appl 7", 0x08 : "PIN Appl 8", 0x09 : "RFU", 0x0A : "ADM1", 0x0B : "ADM2", 0x0C : "ADM3", 0x0D : "ADM4", 0x0E : "ADM5", 0x11 : "PIN Universal PIN", 0x81 : "Second PIN Appl 1", 0x82 : "Second PIN Appl 2", 0x83 : "Second PIN Appl 3", 0x84 : "Second PIN Appl 4", 0x85 : "Second PIN Appl 5", 0x86 : "Second PIN Appl 6", 0x87 : "Second PIN Appl 7", 0x88 : "Second PIN Appl 8", 0x89 : "RFU", 0x8A : "ADM6", 0x8B : "ADM7", 0x8C : "ADM8", 0x8D : "ADM9", 0x8E : "ADM10", } files = [ ([0x3F, 0x00], 'MF', 'MF'), ([0x2F, 0x00], 'EF', 'EF_DIR'), ([0x2F, 0x01], 'EF', 'EF_ATR'), ([0x2F, 0x05], 'EF', 'EF_PL'), ([0x2F, 0x06], 'EF', 'EF_ARR'), ([0x2F, 0x2E], 'EF', 'EF_ICCID'), ([0x7F, 0xFF], 'DF', 'current ADF'), ([0x7F, 0x10], 'DF', 'DF_TELECOM'), ([0x7F, 0x10, 0x5F, 0x50], 'DF', 'DF_GRAPHICS'), ([0x7F, 0x10, 0x5F, 0x3A], 'DF', 'DF_PHONEBOOK'), ([0x7F, 0x20], 'DF', 'DF_GSM'), ([0x7F, 0x21], 'DF', 'DF_DCS1800'), ([0x7F, 0x22], 'DF', 'DF_IS-41'), ([0x7F, 0x23], 'DF', 'DF_FP-CTS'), ([0x7F, 0x24], 'DF', 'DF_TIA-EIA136'), ([0x7F, 0x25], 'DF', 'DF_TIA-EIA95'), ([0x7F, 0x80], 'DF', 'DF_PDC'), ([0x7F, 0x90], 'DF', 'DF_TETRA'), ([0x7F, 0x31], 'DF', 'DF_iDEN'), ] def __init__(self): """ initializes like an ISO7816-4 card with CLA=0x00 initialized on the MF """ ISO7816.__init__(self, CLA=0x00) self.AID = [] self.AID_GP = {} if self.dbg >= 2: log(3, '(UICC.__init__) type definition: %s' % type(self)) log(3, '(UICC.__init__) CLA definition: %s' % hex(self.CLA)) def parse_file(self, Data=[]): """ parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file) mainly based on the ISO7816 parsing style 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 102.221 works over the UICC file structure (quite different from e.g. SIM card) """ # First ISO7816 parsing fil = ISO7816.parse_file(self, Data) # Then UICC extra attributes parsing if 0xC6 in fil.keys(): fil = self.parse_pin_status(fil[0xC6], fil) del fil[0xC6] if 'File Identifier' in fil.keys(): for ref in self.files: if fil['File Identifier'] == ref[0]: fil['Name'] = ref[2] # return the enriched file return fil @staticmethod def parse_pin_status(Data, fil): """ parses a list of bytes provided in Data interprets the content as the UICC pin status and enriches the file dictionnary passed as argument """ PS_DO = Data[2:2+Data[1]] Data = Data[2+len(PS_DO):] PIN_status = '' while len(Data) > 0: [T, L, V] = first_TLV_parser(Data) assert( T in (0x83, 0x95) ) if T == 0x95: # PIN usage if (V[0] << 7) & 1: PIN_status += '#use verification / encipherment ' \ '/ external authentication: ' elif (V[0] << 6) & 1: PIN_status += '#use computation / decipherment ' \ '/ internal authentication: ' elif (V[0] << 5) & 1: PIN_status += '#use SM response: ' elif (V[0] << 4) & 1: PIN_status += '#use SM command: ' elif (V[0] << 3) & 1: PIN_status += '#use PIN verification: ' elif (V[0] << 3) & 1: PIN_status += '#use biometric user verification: ' elif V[0] == 0: PIN_status += '#verification not required: ' elif T == 0x83: # PIN status if len(PIN_status) == 0: PIN_status = '#' if 0x00 < V[0] < 0x12 or 0x81 <= V[0] < 0x90: PIN_status += UICC.pin_status[V[0]] + '#' elif 0x12 <= V[0] < 0x1E: PIN_status += 'RFU (Global)#' elif 0x90 <= V[0] < 0x9F: PIN_status += 'RFU (Local)#' else: PIN_status += '#' #if self.dbg >= 3: # log(3, '(parse_pin_status) %s: %s; PIN status: %s' \ # % (T, V, PIN_status)) Data = Data[L+2:] fil['PIN Status'] = PIN_status return fil def get_AID(self): """ checks EF_DIR at the MF level, and available AID (Application ID) referenced puts it into self.AID """ #go back to MF and select EF_DIR #self.select(addr=[]) # EF_DIR is at the MF level and contains Application ID: EF_DIR = self.select([0x2F, 0x00], type='pmf') if self.dbg >= 3: log(3, '(get_AID) EF_DIR: %s' % EF_DIR) if EF_DIR is None: return # EF_DIR is an EF with linear fixed structure: contains records: for rec in EF_DIR['Data']: # check for a (new) AID: if (rec[0], rec[2]) == (0x61, 0x4F) and len(rec) > 6 \ and rec[4:4+rec[3]] not in self.AID: self.AID.append( rec[4:4+rec[3]] ) #for aid in self.AID: # self.interpret_AID(aid) @staticmethod def interpret_AID(aid=[]): """ returns a string with the interpretation of the AID provided """ if len(aid) < 11: return # check AID format aid_rid = tuple(aid[0:5]) aid_app = tuple(aid[5:7]) aid_country = tuple(aid[7:9]) aid_provider = tuple(aid[9:11]) # get AID application code, depending on SDO... if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x09) \ and aid_app in UICC.AID_ETSI_app_code.keys(): aid_app = UICC.AID_ETSI_app_code[aid_app] if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x87) \ and aid_app in UICC.AID_3GPP_app_code.keys(): aid_app = UICC.AID_3GPP_app_code[aid_app] if aid_rid == (0xA0, 0x00, 0x00, 0x03, 0x43) \ and aid_app in UICC.AID_3GPP2_app_code.keys(): aid_app = UICC.AID_3GPP2_app_code[aid_app] if aid_rid == (0xA0, 0x00, 0x00, 0x06, 0x45) \ and aid_app in UICC.AID_OneM2M_app_code.keys(): aid_app = UICC.AID_OneM2M_app_code[aid_app] # get AID responsible SDO and country if aid_rid in UICC.AID_RID.keys(): aid_rid = UICC.AID_RID[aid_rid] if aid_country in UICC.AID_country_code.keys(): aid_country = UICC.AID_country_code[aid_country] return('%s || %s || %s || %s || %s' \ % (aid_rid, aid_app, aid_country, aid_provider, tuple(aid[11:]))) def get_AID_GP(self): """ tries to select all AID addresses from AID_GP_app_code at the MF level puts those to which there is a positive SW response into self.AID_GP """ for aid in self.AID_GP_code.keys(): aid = list(aid) self.select_by_name(aid) if self.coms()[2] == (0x90, 0x00): # positive response, where we could read the data returned by # the application self.AID_GP[tuple(aid)] = BERTLV_extract(self.coms()[3]) def get_ICCID(self): """ check EF_ICCID at the MF level, and returnq the ASCII value of the ICCID """ #go back to MF and select EF_ICCID #self.select(addr=[]) # EF_ICCID is at the MF level and contains Application ID: EF_ICCID = self.select([0x2F, 0xE2], type='pmf') if self.dbg >= 3: log(3, '(get_ICCID) EF_ICCID: %s' % EF_ICCID) if EF_ICCID is None: return None return decode_BCD( EF_ICCID['Data'] ) def select_by_name(self, name=[]): """ AID selection by name: should be AID bytes """ return self.select(name, 'aid') def select_by_aid(self, aid_num=1): """ AID selection by index """ if hasattr(self, 'AID') and aid_num <= len(self.AID)+1: return self.select(self.AID[aid_num-1], 'aid')