| """ |
| 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.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', |
| 0x84 : 'GET CHALLENGE', |
| 0x86 : 'GENERAL AUTHENTICATE', |
| 0x87 : 'GENERAL AUTHENTICATE', |
| 0x88 : 'INTERNAL AUTHENTICATE', |
| 0x89 : 'AUTHENTICATE', |
| 0xA0 : 'SEARCH BINARY', |
| 0xA1 : 'SEARCH BINARY', |
| 0xA2 : 'SEARCH RECORD', |
| 0xA4 : 'SELECT FILE', |
| 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 : 'RETRIEVE 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, 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 |
| ''' |
| 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/local/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 |
| |
| check also the more complete "parseATR" tool |
| ''' |
| print '\nsmartcard reader: ', self.reader |
| if self.ATR != None: |
| print "\nsmart card ATR is: %s" % toHexString(self.ATR) |
| print 'ATR analysis: ' |
| print ATR(self.ATR).dump() |
| print '\nhistorical bytes: ', \ |
| 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 |
| |
| def sw_status(self, 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): |
| print 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 > 1: |
| print 'DEBUG: 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): |
| print 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 ENVELOPPE(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: |
| ENVELOPPE = [self.CLA, 0xC2, 0x00, 0x00] |
| elif 1 <= len(Data) <= 255: |
| ENVELOPPE = [self.CLA, 0xC2, 0x00, 0x00, len(Data)] + Data |
| return self.sr_apdu(ENVELOPPE) |
| |
| 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 UNBLOCK_CHV(self, P2=0x00, Lc=None, 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 value and new CHV value to set |
| call sr_apdu method |
| |
| TODO: check the exact coding for the Data |
| ''' |
| if Lc is None: |
| UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2] |
| else: |
| UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2, 0x10] + Data |
| return self.sr_apdu(UNBLOCK_CHV) |
| |
| ########################## |
| # 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 > 1: |
| print '[DBG] BER structure:\n%s' % ber |
| if len(ber) > 1: |
| # TODO: implements recursive BER object parsing |
| print '[WNG] more than 1 BER object: %s' % 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 other control structure, DIY |
| fil = {} |
| if ber[0][0][2] == 0x4: |
| fil['Control'] = 'FMD' |
| if self.dbg: |
| print '[WNG] FMD file structure parsing not implemented' |
| elif ber[0][0][2] == 0xF: |
| fil['Control'] = 'FCI' |
| if self.dbg: |
| print '[WNG] FCI file structure parsing not implemented' |
| else: |
| fil['Control'] = ber[0][0] |
| if self.dbg: |
| print '[WNG] 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 > 2: |
| if T in self.file_tags.keys(): |
| Tag = self.file_tags[T] |
| else: |
| Tag = T |
| print '[DBG] %s / %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_security_attribute_compact(V, fil) |
| # Security Attributes |
| elif T in (0x86, 0x8B, 0x8E, 0xA0, 0xA1, 0xAB): |
| fil[self.file_tags[T]] = V |
| # TODO: no concrete parsing at this time... |
| 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 |
| |
| @staticmethod |
| def parse_security_attribute_compact(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 = '#' |
| |
| if 'Type' in fil.keys(): |
| # DF parsing |
| if fil['Type'] == 'DF': |
| 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 #' |
| # EF parsing |
| else: |
| 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 #' |
| |
| # loop on SC: |
| for cond in SC: |
| 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 #' |
| |
| #file['Security Attributes raw'] = Data |
| fil['Security Attributes'] = sec |
| return fil |
| |
| @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 |
| #if self.dbg: |
| # print '[DBG] parse_security_attribute() not implemented' |
| 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 > 1: |
| print '[DBG] %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: |
| print '[WNG] 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, Data=[0x3F, 0x00], typ="fid", with_length=True): |
| ''' |
| self.select(Data=[0x.., 0x..], typ="fid", with_length=True) |
| -> dict(file) on success, None on error |
| |
| selects the file |
| if error, returns None |
| if processing correct: gets response with info on the file |
| if processing correct and EF file: reads the data in the file |
| works in USIM fashion |
| else returns the data dictionnary: check parse_file_(U)SIM methods |
| last apdu available from the attribute self.coms |
| |
| different types of file selection are possible: |
| "fid": select by file id, only the direct child or |
| parent files of the 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 |
| ''' |
| # get the UICC trigger |
| is_UICC = isinstance(self, UICC) |
| |
| # handle type of selection: |
| if typ == "pmf": P1 = 0x08 |
| elif typ == "pdf": P1 = 0x09 |
| elif typ == "aid": P1 = 0x04 |
| # the case of 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(Data) == 0: |
| P1, P2 = 0x00, 0x0C |
| |
| # select file and check SW; if error, returns None, else get response |
| self.coms.push(self.SELECT_FILE(P1=P1, P2=P2, Data=Data, \ |
| 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 > 1: |
| print '[DBG] %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 > 1: |
| print '[DBG] %s' % self.coms() |
| return None |
| |
| data = self.coms()[3] |
| # take the `parse_file()' method from the instance: |
| # ISO7816, UICC or SIM |
| fil = self.parse_file(data) |
| if fil['Type'][0:2] == 'EF': |
| fil = self.read_EF(fil) |
| |
| # finally returns the whole file dictionnary, |
| # containing the ['Data'] key for EF file |
| return fil |
| |
| # |
| ############### |
| # TODO: |
| # improve all of the following... |
| ############### |
| |
| def flat_files_bf(self, path=[], under_AID=0, \ |
| hi_addr=(0, 0xff), lo_addr=(0, 0xff)): |
| ''' |
| flat_files_bf(self, path=[], under_AID=0, \ |
| hi_addr=(0, 0xff), lo_addr=(0, 0xff)) |
| -> list(files), list(DF_to_explore) |
| |
| path: path of the DF under MF or AID to brute force |
| under_AID: if > 0, select the AID number to init the brute force |
| only available for UICC instance |
| hi_addr: 8 MSB of the file address to brute force |
| lo_addr: 8 LSB of the file address to brute force |
| with_select_length: use the length parameter with SELECT instruction |
| |
| brute force file addresses of direct child under a given DF |
| get information on existing files, and discovered DF |
| |
| WARNING: not very tested yet... |
| ''' |
| # init return variables |
| FS, DF_to_explore = [], [] |
| # init selection process |
| MF, sel_type = [0x3F, 0x00], 'fid' |
| if isinstance(self, UICC): |
| sel_type = 'pdf' |
| |
| # start by selecting MF |
| try: |
| r = self.select(MF) |
| except: |
| print '[ERR] selecting MF failed' |
| return |
| if r == None: |
| print '[ERR] MF not found!' |
| return |
| |
| #if needed, select AID |
| if isinstance(self, UICC) and under_AID: |
| try: |
| self.get_AID() |
| r = self.select_by_aid(under_AID) |
| except: |
| print '[ERR] selecting AID failed' |
| return |
| if r == None: |
| print '[ERR] AID not found' |
| return |
| |
| # place on the DF path to bf |
| # select it by 'path from last selected DF' |
| if len(path) > 0: |
| try: |
| path_init = self.select(path, sel_type) |
| except: |
| print '[ERR] selecting path failed:\n%s' % self.coms() |
| return |
| if path_init == None: |
| print '[ERR] path not found: %s' % path |
| return |
| |
| # Dany'style programming |
| def reinit(): |
| self.select(MF) |
| if isinstance(self, UICC) and under_AID: |
| self.select_by_aid(under_AID) |
| if len(path) > 0: |
| self.select(path, sel_type) |
| |
| # loop over the address space to brute force files |
| i, j = 0, 0 |
| for i in range(hi_addr[0], hi_addr[1]): |
| if self.dbg and i%2 == 0: |
| print '[DBG] addr: %s %s %s' % ([hex(v) for v in path], \ |
| hex(i), hex(j)) |
| for j in range(lo_addr[0], lo_addr[1]): |
| # avoid MF re-selection: |
| if (i, j) == [0x3F, 0x00]: |
| fil = None |
| # select by direct file id |
| else: |
| fil = self.select([i, j], sel_type) |
| if fil is not None: |
| if self.dbg: |
| print '[DBG] found file at path, id: ' \ |
| '%s %s' % (path, [i, j]) |
| if 'File Identifier' in fil.keys(): |
| fil['Absolut Path'] = path+fil['File Identifier'] |
| FS.append(fil) |
| if 'Type' in fil.keys() and fil['Type'] == 'DF': |
| reinit() |
| if 'Absolut Path' in fil.keys(): |
| DF_to_explore.append(fil['Absolut Path']) |
| |
| # re-initialize at MF and return |
| self.select(MF) |
| return FS, DF_to_explore |
| |
| def init_FS(self): |
| self.FS = [] |
| |
| def recu_files_bf(self, path=[], under_AID=0): |
| ''' |
| recu_files_bf(self, path=[], under_AID=0) |
| -> void |
| |
| fills self.FS attribute with all files and DF discovered |
| recursively |
| ''' |
| # list all files and DF on the path |
| ret = self.flat_files_bf(path=path, under_AID=under_AID) |
| try: |
| self.FS += ret[0] |
| except: |
| '[ERR] FS not initialized: %s' % type(self.FS) |
| return |
| DF = ret[1] |
| |
| # recursive method call |
| # DF contains absolut path |
| for addr in DF: |
| print '[DBG] path: %s' % addr |
| self.recu_files_bf(path=addr, under_AID=under_AID) |
| |
| @staticmethod |
| def __write_dict(dict, fd): |
| keys = dict.keys() |
| keys.sort() |
| fd.write('\n') |
| for k in keys: |
| fd.write('%s: %s\n' % (k, dict[k])) |
| |
| |
| def scan_fs(self, filename='card_fs', stdout=False): |
| ''' |
| bf_files_under_MF(self, output='card_fs', stdout=True) |
| -> void |
| |
| filename: file to write found information in |
| stdout: print information on stdout too |
| |
| brute force all file addresses from MF and found AID |
| recursively (until no more DF are found) |
| write information on existing file on the output, |
| |
| WARNING: not very tested either... |
| ''' |
| fd = open(filename, 'w') |
| |
| self.init_FS() |
| self.recu_files_bf() |
| fd.write('\n### MF ###\n') |
| for f in self.FS: |
| self.__write_dict(f, fd) |
| fd.write('\n') |
| |
| # TODO: loop that |
| #self.init_FS() |
| #self.recu_files_bf(under_AID=1) |
| #fd.write('\n### AID #1 ###\n') |
| #for f in self.FS: |
| # self.__write_dict(f, fd) |
| # fd.write('\n') |
| |
| fd.close() |
| # |
| |
| ############################################## |
| # 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, 0x04, 0x12): 'OMA', |
| (0xA0, 0x00, 0x00, 0x04, 0x24): 'WiMAX', |
| } |
| ETSI_AID_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', |
| } |
| GPP_AID_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, 0x05): 'Contact Manager API for JavaCard', |
| } |
| GPP2_AID_app_code = { |
| (0x10, 0x02): 'CSIM', |
| } |
| AID_country_code = { |
| (0xFF, 0x33): 'France', |
| (0xFF, 0x44): 'United Kingdom', |
| (0xFF, 0x49): 'Germany', |
| } |
| |
| 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 |
| and check available AID (Application ID) read from EF_DIR |
| |
| initializes on the MF |
| ''' |
| ISO7816.__init__(self, CLA=0x00) |
| self.AID = [] |
| |
| if self.dbg: |
| print '[DBG] type definition: %s' % type(self) |
| print '[DBG] CLA definition: %s' % hex(self.CLA) |
| #print '[DBG] EF_DIR file selection and reading...' |
| |
| 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 >= 2: |
| # print '[DBG] %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 |
| interprets and print the content of the self.AID list |
| ''' |
| #go back to MF and select EF_DIR |
| #self.select(Data=[]) |
| |
| # EF_DIR is at the MF level and contains Application ID: |
| EF_DIR = self.select([0x2F, 0x00], typ='pmf') |
| if self.dbg: |
| print '[DBG] EF_DIR: %s' % EF_DIR |
| if EF_DIR is None: |
| return None |
| |
| # 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]] ) |
| |
| i = 1 |
| for aid in self.AID: |
| 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 self.ETSI_AID_app_code.keys(): |
| aid_app = self.ETSI_AID_app_code[aid_app] |
| if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x87) \ |
| and aid_app in self.GPP_AID_app_code.keys(): |
| aid_app = self.GPP_AID_app_code[aid_app] |
| if aid_rid == (0xA0, 0x00, 0x00, 0x03, 0x43) \ |
| and aid_app in self.GPP2_AID_app_code.keys(): |
| aid_app = self.GPP2_AID_app_code[aid_app] |
| # get AID responsible SDO and country |
| if aid_rid in self.AID_RID.keys(): aid_rid = self.AID_RID[aid_rid] |
| if aid_country in self.AID_country_code.keys(): |
| aid_country = self.AID_country_code[aid_country] |
| |
| print 'found [AID %s] %s || %s || %s || %s || %s' \ |
| % (i, aid_rid, aid_app, aid_country, \ |
| aid_provider, tuple(aid[11:]) ) |
| i += 1 |
| |
| 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(Data=[]) |
| |
| # EF_ICCID is at the MF level and contains Application ID: |
| EF_ICCID = self.select([0x2F, 0xE2], typ='pmf') |
| if self.dbg: |
| print '[DBG] 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 taken from UICC.files |
| ''' |
| for i in range(len(self.files)): |
| if name == self.files[i][2]: |
| return self.select( self.files[i][0], 'pmf' ) |
| |
| def select_by_aid(self, aid_num=1): |
| ''' |
| AID selection by index |
| ''' |
| if len(self.AID) != 0: |
| return self.select(self.AID[aid_num-1], 'aid') |
| |
| |
| |