# # Defines things we know about ISO-7816 cards and GSM cards. # # # License # ------- # # Copyright (c) 2007,2014,2015,2016,2018,2020,2021 Russell Stuart. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # The copyright holders grant you an additional permission under Section 7 # of the GNU Affero General Public License, version 3, exempting you from # the requirement in Section 6 of the GNU General Public License, version 3, # to accompany Corresponding Source with Installation Information for the # Program or any work based on the Program. You are still required to # comply with all other Section 6 requirements to provide Corresponding # Source. # # 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 # Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # import binascii import string import sys # # Python2/3 compatibility stuff. # # b2a_hex: convert binary to a hex string. # num8: Convert a byte to a number. # to_bytes: Convert a sequence of numbers to bytes. # if sys.hexversion >= 0x03000000: num8 = int to_bytes = bytes # # ffs. What idiot decided 'b2a_hex()' shouldn't return a string? # def b2a_hex(hex): return binascii.b2a_hex(hex).decode('latin') else: num8 = ord b2a_hex = binascii.b2a_hex # # Do what Python3's "bytes(sequence)" does. # def to_bytes(ints): return ''.join(chr(i) for i in ints) def num16(bytes): return num8(bytes[0]) * 256 + num8(bytes[1]) # # Parameters for read() and seek(). # RECORD_MODE_NEXT = 0x02 RECORD_MODE_PREVIOUS = 0x03 RECORD_MODE_SEEK = 0x04 SEEK_MODE_BEGINING_FORWARD = 0x00 SEEK_MODE_BEGINING_BACKWARD = 0x01 SEEK_MODE_NEXT_FORWARD = 0x02 SEEK_MODE_NEXT_BACKWARD = 0x03 SEEK_TYPE_GET = 0x10 # # Access conditions / Pin names / Keys / whatever name they have today. # ACCESS_CONDITION = { 0: 'ALW', 1: 'PN1', 2: 'PN2', 3: 'RFU', 4: 'A04', 5: 'A05', 6: 'A06', 7: 'NE7', 8: 'A08', 9: 'A09', 10: 'A0a', 11: 'A0b', 12: 'A0c', 13: 'A0d', 14: 'A0e', 15: 'NEF', } # # 7816 instructions. # INS_ERASE_BINARY = 0x0e INS_VERIFY = 0x20 INS_MANAGE_CHANNEL = 0x70 INS_EXTERNAL_AUTH = 0x82 INS_GET_CHALLENGE = 0x84 INS_INTERNAL_AUTH = 0x88 INS_SELECT_FILE = 0xa4 INS_READ_BINARY = 0xb0 INS_READ_RECORD = 0xb2 INS_GET_RESPONSE = 0xc0 INS_ENVELOPE = 0xc2 INS_GET_DATA = 0xca INS_WRITE_BINARY = 0xd0 INS_WRITE_RECORD = 0xdc INS_UPDATE_BINARY = 0xd6 INS_PUT_DATA = 0xda INS_UPDATE_RECORD = 0xdc INS_APPEND_RECORD = 0xe2 # # P2 encoding for read/write/update records. # RECORD_P2_1ST_RECNO = 0x00 RECORD_P2_LAST_RECNO = 0x01 RECORD_P2_NEXT_RECNO = 0x02 RECORD_P2_PREV_RECNO = 0x03 RECORD_P2_RECNO = 0x04 RECORD_P2_FROM_RECNO = 0x05 RECORD_P2_TO_RECNO = 0x06 RECORD_P2_RFU = 0x07 # # GSM instructions. # CLS_GSM_SIM = 0xa0 CLS_GSM_USIM = 0x00 INS_GSM_INVALIDATE = 0x04 INS_GSM_TERM_PROFILE = 0x10 INS_GSM_FETCH = 0x12 INS_GSM_TERM_RESPONSE = 0x14 INS_GSM_VERIFY_CHV = INS_VERIFY INS_GSM_CHANGE_CHV = 0x24 INS_GSM_DISABLE_CHV = 0x26 INS_GSM_ENABLE_CHV = 0x28 INS_GSM_UNBLOCK_CHV = 0x2C INS_GSM_INCREASE = 0x32 INS_GSM_REHABILITATE = 0x44 INS_GSM_MANAGE_CHANNEL = INS_MANAGE_CHANNEL INS_GSM_RUN_GSM = INS_INTERNAL_AUTH INS_GSM_SEEK = 0xa2 INS_GSM_SELECT_FILE = INS_SELECT_FILE INS_GSM_READ_BINARY = INS_READ_BINARY INS_GSM_READ_RECORD = INS_READ_RECORD INS_GSM_GET_RESPONSE = INS_GET_RESPONSE INS_GSM_ENVELOPE = INS_ENVELOPE INS_GSM_UPDATE_BINARY = INS_UPDATE_BINARY INS_GSM_UPDATE_RECORD = INS_UPDATE_RECORD INS_GSM_STATUS = 0xf2 # # 7816-9 AM_DO encoding. # AM_DO_TAG_SAC_MASK = 0xf0 AM_DO_TAG_SAC = 0x80 AM_DO_TAG_CI12 = 0x0f AM_DO_TAG_CI12_P2 = 0x01 AM_DO_TAG_CI12_P1 = 0x02 AM_DO_TAG_CI12_INS = 0x04 AM_DO_TAG_CI12_CLS = 0x08 AM_DO_TAG_VENDOR = 0x9c AM_DO_SAC_ALWAYS = 0x00 AM_DO_SAC_NEVER = 0xff AM_DO_SAC_ID = 0x0f AM_DO_SAC_ID_ALWAYS = 0x00 AM_DO_SAC_ID_PIN1 = 0x01 AM_DO_SAC_ID_PIN2 = 0x02 AM_DO_SAC_ID_ADM1 = 0x03 AM_DO_SAC_ID_ADM2 = 0x04 AM_DO_SAC_ID_ADM3 = 0x05 AM_DO_SAC_ID_ADM4 = 0x06 AM_DO_SAC_ID_ADM5 = 0x07 AM_DO_SAC_ID_ADM6 = 0x08 AM_DO_SAC_ID_ADM7 = 0x09 AM_DO_SAC_ID_ADM8 = 0x0a AM_DO_SAC_ID_ADM9 = 0x0b AM_DO_SAC_ID_ADM10 = 0x0c AM_DO_SAC_ID_NEVER = 0x0f AM_DO_SAC_COND = 0x70 AM_DO_SAC_COND_SECURE = 0x40 AM_DO_SAC_ORAND = 0x80 AM_DO_SAC_OR = 0x00 AM_DO_SAC_AND = 0x80 AM_DO_DF_DELETE_CHILD = 0x01 AM_DO_DF_CREATE_EF = 0x02 AM_DO_DF_CREATE = 0x04 AM_DO_DF_DEACTIVATE = 0x08 AM_DO_DF_ACTIVATE = 0x10 AM_DO_DF_TERMINATE = 0x20 AM_DO_DF_DELETE_SELF = 0x40 AM_DO_EF_READ = 0x01 AM_DO_EF_UPDATE = 0x02 AM_DO_EF_RFU0 = 0x04 AM_DO_EF_DEACTIVATE = 0x08 AM_DO_EF_ACTIVATE = 0x10 AM_DO_EF_TERMINATE = 0x20 AM_DO_EF_GSM_INCREASE = 0x20 AM_DO_EF_DELETE_SELF = 0x40 AM_DO_KY_READ = 0x01 AM_DO_KY_UPDATE = 0x02 AM_DO_KY_MSO_PSO = 0x04 AM_DO_KY_DEACTIVATE = 0x08 AM_DO_KY_ACTIVATE = 0x10 AM_DO_KY_TERMINATE = 0x20 AM_DO_KY_DELETE_SELF = 0x40 AM_DO_SE_READ = 0x01 AM_DO_SE_UPDATE = 0x02 AM_DO_SE_MSO_RESTORE = 0x04 AM_DO_SE_DEACTIVATE = 0x08 AM_DO_SE_ACTIVATE = 0x10 AM_DO_SE_TERMINATE = 0x20 AM_DO_SE_DELETE_SELF = 0x40 # # 7816-9 SC_DO encoding. # SC_DO_TAG_ALWAYS = 0x90 SC_DO_TAG_NEVER = 0x97 SC_DO_TAG_SAC = 0x9e SC_DO_TAG_OR = 0xa0 SC_DO_TAG_CRT = 0xa4 SC_DO_TAG_AND = 0xaf SC_CRT_TAG_KEY = 0x83 SC_CRT_TAG_ACTION = 0x95 SC_CRT_ACTION_AUTH = 0x80 SC_CRT_ACTION_VERIFY = 0x08 # # PIN & Key cross reference. # GSM_KEY_2_PIN = [ (0x00, 0x00, AM_DO_SAC_ID_ALWAYS, 0), (0x01, 0x08, AM_DO_SAC_ID_PIN1, 0), (0x09, 0x0e, AM_DO_SAC_ID_ADM1, 1), (0x11, 0x11, AM_DO_SAC_ID_PIN1, 0), (0x81, 0x88, AM_DO_SAC_ID_PIN2, 0), (0x89, 0x8e, AM_DO_SAC_ID_ADM6, 1), ] GSM_KEY_2_PIN = dict( (k, p + (k - s) * i) for s, f, p, i in GSM_KEY_2_PIN for k in range(s, f + 1)) # # File types. # FILE_TYPE_DF = 0x38 FILE_TYPE_EF = 0x00 # # File statuses # FILE_STATUS_VALID = 0x01 FILE_STATUS_ACCESSIBLE_INVALID = 0x04 # # UICC Characteristics. # FILE_CHARACTERISTICS_CLOCK = 0x0D FILE_CHARACTERISTICS_CLOCK_STOP = 0x01 FILE_CHARACTERISTICS_CLOCK_HIGH = 0x04 FILE_CHARACTERISTICS_CLOCK_LOW = 0x08 FILE_CHARACTERISTICS_MHZ = 0x02 FILE_CHARACTERISTICS_MHZ_13_8 = 0x00 FILE_CHARACTERISTICS_MHZ_13_4 = 0x02 FILE_CHARACTERISTICS_VOLTS = 0x70 FILE_CHARACTERISTICS_VOLTS_5V = 0x00 FILE_CHARACTERISTICS_VOLTS_3V = 0x10 FILE_CHARACTERISTICS_VOLTS_18V = 0x20 FILE_CHARACTERISTICS_VOLTS_FUTURE = 0x40 FILE_CHARACTERISTICS_CHV1 = 0x80 # # File structures. # FILE_STRUCTURE_UNKNOWN = 0x00 FILE_STRUCTURE_TRANSPARENT = 0x01 FILE_STRUCTURE_LINEAR = 0x02 FILE_STRUCTURE_LINEAR_TLV = 0x03 FILE_STRUCTURE_LINEAR_V = 0x04 FILE_STRUCTURE_LINEAR_V_TLV = 0x05 FILE_STRUCTURE_CYCLIC = 0x06 FILE_STRUCTURE_CYCLIC_TLV = 0x07 # # Life cycle encoding. # FILE_LIFECYCLE_UNKNOWN = 0x00 FILE_LIFECYCLE_CREATED = 0x01 FILE_LIFECYCLE_INITIALISED = 0x02 FILE_LIFECYCLE_ACTIVE_MASK = 0xf5 FILE_LIFECYCLE_ACTIVE_VALUE = 0x05 FILE_LIFECYCLE_DEACTIVE_VALUE = 0x04 FILE_LIFECYCLE_TERM_MASK = 0xfc FILE_LIFECYCLE_TERM_VALUE = 0x0c # # Get the description of a SMART CARD APDU status. # def status2desc(status): '''Return a description of the passed smart card status code''' desc = STATUSES.get(status[-2:], None) if desc is None: desc = STATUSES.get(status[-2:-1], None) if desc is None: desc = '???' elif '%' in desc: desc = desc % num8(status[-1]) return b2a_hex(status[-2:]) + ' ' + desc # # Generate an AID useful for prefix searching. The new order is: # # - 2 bytes. Application class. # - 5 bytes. Vendor. # - 11 bytes. Application ID. # def aid_prefix(prefix): if len(prefix) > 7: return prefix[5:7] + prefix[0:5] + prefix[7:] if len(prefix) > 2: return prefix[-2:] + prefix[:-2] return prefix # # Well known statuses. # STATUS_SUCCESS = b'\x90\x00' STATUS_DATA_INVALID = b'\x69\x84' STATUS_DATA_WAITING = b'\x61' STATUS_DF_NOT_FOUND = b'\x6a\x82' STATUS_FILE_INVALID = b'\x62\x83' STATUS_FILE_NOT_FOUND = b'\x94\x04' STATUS_INVALID_ADDRESS = b'\x94\x02' STATUS_NOSUCH_FILE = b'\x6a\x82' STATUS_PIN_REQUIRED = b'\x69\x82' STATUS_SUCCESS_DATA = b'\x9f\x00' STATUSES = { binascii.a2b_hex('9000'): 'Success', binascii.a2b_hex('91'): 'Success, response of %d bytes waiting', binascii.a2b_hex('9E'): 'download error, response of %d bytes waiting', binascii.a2b_hex('9F'): 'Success, response of %d bytes waiting', binascii.a2b_hex('9200'): 'Success after 0 retries', binascii.a2b_hex('9201'): 'Success after 1 retries', binascii.a2b_hex('9202'): 'Success after 2 retries', binascii.a2b_hex('9203'): 'Success after 3 retries', binascii.a2b_hex('9204'): 'Success after 4 retries', binascii.a2b_hex('9205'): 'Success after 5 retries', binascii.a2b_hex('9206'): 'Success after 6 retries', binascii.a2b_hex('9207'): 'Success after 7 retries', binascii.a2b_hex('9208'): 'Success after 8 retries', binascii.a2b_hex('9209'): 'Success after 9 retries', binascii.a2b_hex('9240'): 'Memory problem', binascii.a2b_hex('9400'): 'No EF Selected', binascii.a2b_hex('9402'): 'invalid address, or out of range', binascii.a2b_hex('9404'): 'file or pattern not found', binascii.a2b_hex('9408'): 'file inconsistant with command', binascii.a2b_hex('9802'): 'no CHV initialised', binascii.a2b_hex('9804'): 'no permission or CHV wrong', binascii.a2b_hex('9808'): 'in contradiction with CHV status', binascii.a2b_hex('9810'): 'in contradiction with invalidation status', binascii.a2b_hex('9840'): 'unsuccessful CHV validation, no attempts left', binascii.a2b_hex('9850'): 'max value reached', binascii.a2b_hex('9850'): 'max value reached', binascii.a2b_hex('61'): '%d response bytes remaining', binascii.a2b_hex('62'): 'execution error, memory unchanged', binascii.a2b_hex('6281'): 'execution error, memory unchanged - returned data corrupted', binascii.a2b_hex('6282'): 'execution error, memory unchanged - returned data short', binascii.a2b_hex('6283'): 'execution error, memory unchanged - selected file invalid', binascii.a2b_hex('6284'): 'execution error, memory unchanged - FCI format invalid', binascii.a2b_hex('63'): 'execution error, memory changed', binascii.a2b_hex('6381'): 'execution error, memory changed - file filled up', binascii.a2b_hex('63C0'): 'execution error, memory changed - counter 0', binascii.a2b_hex('63C1'): 'execution error, memory changed - counter 1', binascii.a2b_hex('63C2'): 'execution error, memory changed - counter 2', binascii.a2b_hex('63C3'): 'execution error, memory changed - counter 3', binascii.a2b_hex('63C4'): 'execution error, memory changed - counter 4', binascii.a2b_hex('63C5'): 'execution error, memory changed - counter 5', binascii.a2b_hex('63C6'): 'execution error, memory changed - counter 6', binascii.a2b_hex('63C7'): 'execution error, memory changed - counter 7', binascii.a2b_hex('63C8'): 'execution error, memory changed - counter 8', binascii.a2b_hex('63C9'): 'execution error, memory changed - counter 9', binascii.a2b_hex('63CA'): 'execution error, memory changed - counter 10', binascii.a2b_hex('63CB'): 'execution error, memory changed - counter 11', binascii.a2b_hex('63CC'): 'execution error, memory changed - counter 12', binascii.a2b_hex('63CD'): 'execution error, memory changed - counter 13', binascii.a2b_hex('63CE'): 'execution error, memory changed - counter 14', binascii.a2b_hex('63CF'): 'execution error, memory changed - counter 15', binascii.a2b_hex('64'): 'execution error, memory unchanged', binascii.a2b_hex('65'): 'execution error, memory unchanged', binascii.a2b_hex('6581'): 'execution error, memory unchanged - memory failure', binascii.a2b_hex('66'): 'security related issue', binascii.a2b_hex('67'): 'wrong length', binascii.a2b_hex('68'): 'invalid function for class', binascii.a2b_hex('6881'): 'invalid function for class - channel not supported', binascii.a2b_hex('6882'): 'invalid function for class - secure messaging not supported', binascii.a2b_hex('69'): 'command not allowed', binascii.a2b_hex('6981'): 'command not allowed - incompatible with file structure', binascii.a2b_hex('6982'): 'command not allowed - security not satisified', binascii.a2b_hex('6983'): 'command not allowed - authention method blocked', binascii.a2b_hex('6984'): 'command not allowed - referenced data invalidated', binascii.a2b_hex('6985'): 'command not allowed - conditions of use not satisified', binascii.a2b_hex('6986'): 'command not allowed - no current EF', binascii.a2b_hex('6987'): 'command not allowed - expected SM data objects missing', binascii.a2b_hex('6988'): 'command not allowed - SM data objects incorrect', binascii.a2b_hex('6A'): 'incorrect P1 or P2', binascii.a2b_hex('6A80'): 'incorrect P1 or P2 - incorrect parameters in the data field', binascii.a2b_hex('6A81'): 'incorrect P1 or P2 - function not supported', binascii.a2b_hex('6A82'): 'incorrect P1 or P2 - file not found', binascii.a2b_hex('6A83'): 'incorrect P1 or P2 - record not found', binascii.a2b_hex('6A84'): 'incorrect P1 or P2 - not enough memory space in the file', binascii.a2b_hex('6A85'): 'incorrect P1 or P2 - data length inconsistent with TLV', binascii.a2b_hex('6A86'): 'incorrect P1 or P2 - incorrect P1-P2', binascii.a2b_hex('6A87'): 'incorrect P1 or P2 - length inconsistent with P1-P2', binascii.a2b_hex('6A88'): 'incorrect P1 or P2 - referenced data not found', binascii.a2b_hex('6B'): 'incorrect P1 or P2 parameter', binascii.a2b_hex('6C'): 'incorrect LE paramater, %d is correct length', binascii.a2b_hex('6D'): 'incorrect instruction code', binascii.a2b_hex('6E'): 'incorrect instruction class', binascii.a2b_hex('6F'): 'it didn\'t work, and the card isn\'t saying why', } # # Decode a ber-tlv string. # def ber_tlv(blob, index=None, end=None): if index is None: index = 0 if end is None: end = len(blob) result = [] while index < end and blob[index] in b'\x00\xff': index += 1 while index < end: start = index index += 1 if num8(blob[start]) & 0x1f == 0x1f: while num8(blob[index]) & 0x80 == 0x80: index += 1 index += 1 tag = blob[start:index] length = 0 while num8(blob[index]) & 0x80 != 0: length = (length << 7) + (num8(blob[index]) & 0x7f) index += 1 length = (length << 7) + num8(blob[index]) index += 1 start = index index += length if num8(tag[0]) & 0x20 == 0: result.append((tag, blob[start:index])) else: result.append((tag, ber_tlv(blob, start, index)[0])) while index < end and blob[index] in b'\x00\xff': index += 1 return result, index # # Decode a simple-tlv string. # def simple_tlv(blob, index=None, end=None): if index is None: index = 0 if end is None: end = len(blob) result = [] while index < end and blob[index] in b'\x00\xff': index += 1 while index < end: tag = blob[index] index += 1 if blob[index] not in b'\xff': length = num8(blob[index]) index += 1 else: length = num16(blob[index + 1:index + 3]) index += 3 start = index index += length result.append((tag, blob[start:index])) while index < end and blob[index] in b'\x00\xff': index += 1 return result, index # # Decode a compact-tlv string. # def compact_tlv(blob, index=None, end=None): if index is None: index = 0 if end is None: end = len(blob) result = [] while index < end: tag = 0x40 | (num8(blob[index]) >> 4) length = num8(blob[index]) & 0xf index += 1 start = index index += length result.append((tag, blob[start:index])) return result, index # # Decode the response to the INS_SELECT_FILE APDU. # class Select: # # Instance variables. This what the response is unpacked into. # data = None # string, the raw data returned access = None # dict, {AM_DO_EF_*:AM_DO_SAC_ID_PIN1, ...} characteristics = None # int, Combination of FILE_CHARACTERISTICS_* dfname = None # string, the dfname of a DF for USIM's or "" ef_arr = None # list, List of er_arr file names referenced file_size = None # int, memory occuped by file data, -1 for DF's file_type = None # int, One of the FILE_TYPE_* constants id = None # int, file id lifecycle = None # int, One of FILE_LIFECYCLE_* pins = None # int, 0=No pin, 1=PIN1, 2=PIN2, 3=PIN1+PIN2 reccnt = None # int, No. records in file, or -1 if not defined reclen = None # int, the record length, or -1 if not defined short_id = None # int, short id, or -1 if not defined structure = None # int, One of the FILE_STRUCTURE_* constants # # Initialise the fields in here from a # smart card response to the SELECT APDU. # def __init__(self, data, ef_arr=None): self.data = data if not data: return self.access = {} self.ef_arr = [] if data[0] in b'\x00\x61': self._init_fci() elif data[0] in b'\x62': self._init_fcp(ef_arr) # # The new style: a file control protocol. A list of TLV's. # def _init_fcp(self, ef_arr=None): self.rfu = tuple() for tag, value in ber_tlv(self.data)[0][0][1]: # # Tags from iso7816. # if tag == b'\x80': self.file_size = num16(value) elif tag == b'\x81': pass elif tag == b'\x82': byte = num8(value[0]) fileType = byte & 0xb8 self.file_type = self.FILE_TYPES.get(fileType, -fileType - 1) self.structure = byte & 0x80 != 0 and byte or byte & 0x07 if len(value) < 3: pass elif len(value) == 3: self.reclen = num8(value[2]) else: self.reclen = num16(value[2:4]) if len(value) < 5: pass else: self.reccnt = num8(value[4]) elif tag == b'\x83': self.id = num16(value) if self.short_id is None: self.short_id = self.id & 0x1f elif tag == b'\x84': self.dfname = value # # Additional tags from etsi TS 102 221 (selection 11.1.1). # elif tag == b'\x88': if len(value) == 0: self.short_id = -1 else: self.short_id = num8(value[0]) >> 3 elif tag == b'\x8A': self.lifecycle = num8(value[0]) elif tag == b'\x8B': file_id = value[:2] if file_id not in self.ef_arr: self.ef_arr.append(file_id) ef_arr_file = ef_arr and ef_arr.get(file_id, None) or None if ef_arr_file and len(value) == 3: self.parse_am_do(ber_tlv(ef_arr_file[num8(value[2])])[0]) elif tag == b'\x8C': ams = [ i for i in range(7, -1, -1) if (1 << i) & num8(value[0])] for am, sc in zip(ams, value[1:]): self.setaccess(am, sc) elif tag == b'\xa5': for tag1, value1 in value: if tag1 == b'\x80': self.characteristics = num8(value1[0]) elif tag == b'\xAB': self.parse_am_do(value) elif tag == b'\xC6': self.pins = 0 for tag1, value1 in ber_tlv(value)[0]: if tag1 == b'\x83': self.pins |= self.PINS.get(num8(value1[0]), 0) FILE_TYPES = {0x00: FILE_TYPE_EF, 0x38: FILE_TYPE_DF} PINS = {AM_DO_SAC_ID_PIN1: 0x01, AM_DO_SAC_ID_PIN2: 0x02} # # The old style. # def _init_fci(self): if len(self.data) < 7: return self.id = num16(self.data[4:6]) fileType = num8(self.data[6]) self.file_type = self.FCI_FILE_TYPES.get(fileType, -fileType - 1) if self.file_type == FILE_TYPE_EF: self._init_fci_ef() elif self.file_type == FILE_TYPE_DF: result = self._init_fci_df() FCI_FILE_TYPES = { 1: FILE_TYPE_DF, # MF 2: FILE_TYPE_DF, # DF 4: FILE_TYPE_EF, } # # Decode DF data for the old style. # def _init_fci_df(self): self.rfu = [(0, 2), (7, 12), (18, 19), (22, len(self.data))] if len(self.data) < 22: return b2a_hex(self.data) self.pins = 0 self.pins += num8(self.data[18]) & 0x80 == 0 and 0 or 0x01 self.pins += num8(self.data[20]) & 0x80 == 0 and 0 or 0x02 self.characteristics = num8(self.data[13]) # # Decode EF data for the old style. # def _init_fci_ef(self): self.rfu = [(0, 2), (15, len(self.data))] if len(self.data) < 14: return self.file_size = num16(self.data[2:4]) self.characteristics = num8(self.data[7]) self.access[AM_DO_EF_UPDATE] = num8(self.data[8]) % 16 self.access[AM_DO_EF_READ] = num8(self.data[8]) // 16 self.access[AM_DO_EF_RFU0] = num8(self.data[9]) % 16 self.access[AM_DO_EF_GSM_INCREASE] = num8(self.data[9]) // 16 self.access[AM_DO_EF_DEACTIVATE] = num8(self.data[10]) % 16 self.access[AM_DO_EF_ACTIVATE] = num8(self.data[10]) // 16 status = num8(self.data[11]) self.lifecycle = self.LIFECYCLES.get(status, -status - 1) struct = num8(self.data[13]) self.structure = self.STRUCTURES.get(struct, -struct - 1) if len(self.data) >= 15: self.reclen = num8(self.data[14]) else: self.reclen = -1 LIFECYCLES = { 0x01: FILE_LIFECYCLE_ACTIVE_VALUE, 0x41: FILE_LIFECYCLE_ACTIVE_VALUE, } STRUCTURES = { 0x00: FILE_STRUCTURE_TRANSPARENT, 0x01: FILE_STRUCTURE_LINEAR, 0x03: FILE_STRUCTURE_CYCLIC } # # Print a string representation of the SELECT response. # def __str__(self, short=None): if len(self.data) < 7: return b2a_hex(self.data) str_id = self._str_undef(self.id, '%04x') if self.short_id is None: str_id += ' ' pass elif self.short_id == -1: str_id += ': ' else: str_id += ':%02x' % self.short_id str_file_type = self._str_lookup( self.STR_FILETYPE, self.file_type, 'x%02x') if self.file_type == FILE_TYPE_DF: result = self._str_df() elif self.file_type == FILE_TYPE_EF: result = self._str_ef() else: result = b2a_hex(self.data) return '%s %s %s' % (str_id, str_file_type, result) STR_FILETYPE = { FILE_TYPE_DF: 'DF', FILE_TYPE_EF: 'EF' } # # Print the bits specific to DF or MF. # def _str_df(self): if self.pins is None: return b2a_hex(self.data) str_pins = self._str_lookup(self.STR_PINS, self.pins, 'x%02x') if self.dfname is None: str_dfname = '' else: str_dfname = ' dfname=' + b2a_hex(self.dfname) return 'pins=%s%s%s' % (str_pins, str_dfname, self._str_rfu()) STR_PINS = { 0: 'n', 1: '1', 2: '2', 3: '1+2', } # # Print the bits specific to EF. # def _str_ef(self, short=None): def ac(condition): cond = self.access.get(condition, None) dsc = cond is None and '???' or ACCESS_CONDITION[cond] return dsc[0] + dsc[-1] if self.characteristics is None: return b2a_hex(self.data) str_structure = self._str_lookup( self.STR_STRUCTURE, self.structure, 'x%02x') str_lifecycle = self._str_lookup( self.STR_LIFECYCLE, self.lifecycle, ' life=x%02x') if self.structure == FILE_STRUCTURE_TRANSPARENT: if self.reclen in (None, -1, 0): str_len = '' else: str_len = ' len=x%02x' % (self.reclen,) elif self.structure in (FILE_STRUCTURE_LINEAR, FILE_STRUCTURE_CYCLIC): str_len = ' len=%s:%s' % ( self._str_undef(self.reclen, '%d'), self._str_undef(self.reccnt, '%d')) else: str_len = ' len=x%02x' % self.reclen return '%s %s %s,%s,%s,%s,%s,%s%s%s%s' % ( self._str_undef(self.file_size, '%4d'), str_structure, ac(AM_DO_EF_READ), ac(AM_DO_EF_UPDATE), ac(AM_DO_EF_GSM_INCREASE), ac(AM_DO_EF_RFU0), ac(AM_DO_EF_DEACTIVATE), ac(AM_DO_EF_ACTIVATE), str_len, str_lifecycle, self._str_rfu() ) STR_STRUCTURE = { FILE_STRUCTURE_TRANSPARENT: 'trn', FILE_STRUCTURE_LINEAR: 'lin', FILE_STRUCTURE_LINEAR_TLV: 'ltv', FILE_STRUCTURE_LINEAR_V: 'vin', FILE_STRUCTURE_LINEAR_V_TLV: 'vtv', FILE_STRUCTURE_CYCLIC: 'cyc', FILE_STRUCTURE_CYCLIC_TLV: 'ctv' } STR_LIFECYCLE = { FILE_LIFECYCLE_ACTIVE_VALUE: '', FILE_LIFECYCLE_ACTIVE_VALUE | 2: '', } # # Dump the unused portions of the old style if they aren't 0. # def _str_rfu(self): results = [] for u in self.rfu: begin = u[0] while begin < u[1]: while begin < u[1] and self.data[begin] in b'\x00': begin += 1 end = begin while end < u[1] and self.data[end] not in b'\x00': end += 1 if begin < u[1]: if begin + 1 == end: results.append( '%d=%s' % (begin, b2a_hex(self.data[begin:end]))) else: results.append( '%d:%d=%s' % (begin, end, b2a_hex(self.data[begin:end]))) begin = end if not results: return '' return ' ' + ' '.join(results) # # Lookup a value. # def _str_lookup(self, table, value, format): result = table.get(value, None) if result is not None: return result return self._str_undef(value, format) # # Print an undefined value. # def _str_undef(self, value, format): if value is None: return '?' if value < 0: value = 1 - value return format % value # # Compare two selects. # def __cmp__(self, other): return cmp(self.data, other.data) def __eq__(self, other): return self.data.__eq__(other.data) def __ne__(self, other): return self.data.__ne__(other.data) def __lt__(self, other): return self.data.__lt__(other.data) def __le__(self, other): return self.data.__le__(other.data) def __ge__(self, other): return self.data.__ge__(other.data) def __gt__(self, other): return self.data.__gt__(other.data) # # Set self.access. # def setaccess(self, ams, sc): for am in (1 << bit for bit in range(0, 8) if ams & (1 << bit) != 0): if self.access.get(am, 9999) > sc: self.access[am] = sc def setaccess_crt(self, tag, am, value): num8_tag = num8(tag[0]) if num8_tag == SC_DO_TAG_ALWAYS: self.setaccess(am, AM_DO_SAC_ID_ALWAYS) elif num8_tag == SC_DO_TAG_NEVER: self.setaccess(am, AM_DO_SAC_ID_NEVER) elif num8_tag == SC_CRT_TAG_KEY: self.setaccess(am, GSM_KEY_2_PIN[num8(value[0])]) elif tag in self.CONTAINER_TAGS: for tag1, value1 in value: self.setaccess_crt(tag1, am, value1) CONTAINER_TAGS = to_bytes((SC_DO_TAG_CRT, SC_DO_TAG_OR, SC_DO_TAG_AND,)) def parse_am_do(self, value): for tag, value in value: tag_num = num8(tag[0]) if tag_num == AM_DO_TAG_VENDOR: continue if tag_num == AM_DO_TAG_SAC: am = num8(value[0]) elif (tag_num & AM_DO_TAG_SAC_MASK) == AM_DO_TAG_SAC: # # Specify which commands can be applied by one of more of # the instruction bytes CLA,INS,P1,P2 specfied by the # AM_DO_TAG_CI12 mask. This isn't easily translated to # the old GSM permissions, so we don't handle this. # am = -1 elif am != -1: self.setaccess_crt(tag, am, value) # # Well known File ID's. # FILE_ID_EF_DIR = 0x2F00 # Application Directory FILE_ID_EF_ATR = 0x2F01 # Answer to Reset FILE_ID_MASTER = 0x3F00 # Root FILE_ID_CURRENT_DF = 0x3FFF # Alias for current DF (not GSM) FILE_ID_CURRENT_ADF = 0x7FFF # Alias for current ADF (GSM only) FILE_ID_RFU = 0xFFFF # Reserved # # A set of classes that decode and encode binary data from a smart card. # Here 'decode' means pretty print. The classes are initialised with a # string that specifies the record format. An instance of the class # decodes and encodes the binary data according to that format. # # Some formats require counts to specify a repeat factor or size. Counts can # be one of: # # N - A fixed count. The number N is the count. # # =N - An indexed count. The count is contained in the byte at offset N # in the record. Thus =0 means the count is in the first byte in # the record. # # +N - An indexed count. The count is contained in the byte N bytes # from the end. Thus +1 means the count is in the last byte in # the record. # # -N - An indexed count. The count is contained in the byte N bytes # before the current offset. # # * - A variable count. The count is the number of bytes needed to make # up the record size. There can only be one of these, and it must # follow any indexed counts. # class FormatClass: formatters = [] # Class variable - Classes that implement us. length = None # Instances must define the length of the field. startChars = None # Instances must define legal start tokens. # # Check a name. # @classmethod def isValidName(cls, name): if not name: return 'Empty name not allowed' if name[0] not in string.ascii_letters: return 'The name %r doesn\'t start with a letter' % (name,) invalid = [n for n in name if n not in cls.VALID_NAME_CHARS] if invalid: return ( 'The name %r contains the invalid characters %r' % (name, invalid,)) return None VALID_NAME_CHARS = string.ascii_letters + string.digits + '_' # # Subclasses implement this. It parses a format specificiation. Returns # the subclass that will decode/encode that format, and the index into the # format_spec of the remaining data. # def parse(self, format_spec, index): raise NotImplementedException('FormatClass.format_spec') # # Decode the binary blob in data, starting at 'index'. # If this is a variable length this will contain the number of # def decode(self, data, index, len): raise NotImplementedException('FormatClass.decode') class CountPlus: isFixed = False isVariable = False isIndexed = True def __init__(self, count): self.count = count def __call__(self, data, index, default): return num8(data[len(data) - self.count]) class CountMinus: isFixed = False isVariable = False isIndexed = True def __init__(self, count): self.count = count def __call__(self, data, index, default): return num8(data[index - self.count]) class CountAbs: isFixed = False isVariable = False isIndexed = True def __init__(self, count): self.count = count def __call__(self, data, index, default): return num8(data[self.count]) class CountVar: isFixed = False isVariable = True isIndexed = False def __init__(self, count): self.count = count def __call__(self, data, index, default): return default class CountFixed: isFixed = True isVariable = False isIndexed = False def __init__(self, count): self.count = count def __call__(self, data, index, default): return self.count # # Helper routine for parsing counts. # @classmethod def parse_count(cls, count_spec): type = count_spec[:1] if type not in string.digits: count_spec = count_spec[1:] count = count_spec and int(count_spec) or 0 if type == '+': return cls.CountPlus(count) if type == '-': return cls.CountMinus(count) if type == '=': return cls.CountAbs(count) if count == 0: return cls.CountVar(count) return cls.CountFixed(count) # # Utility to parse a format string. # @classmethod def parse_format(cls, format_spec, index): startChar = format_spec[index] candidates = [f for f in cls.formatters if startChar in f.startChars] if len(candidates) == 0: raise GsmException('Syntax error at ' + format_spec[index:]) if len(candidates) > 1: raise GsmException('Ambigiouty at ' + format_spec[index:]) formatter = candidates[0]() end = formatter.parse(format_spec, index) formatter.spec = format_spec[index:end] return formatter, end @classmethod def decode_format(cls, format, data, index, len): return format.decode(data, index, len) @classmethod def encode_format(cls, format, data, index, len): return format.encode(data, index, len) @classmethod def skipspaces(cls, str, index): while index < len(str) and str[index] in string.whitespace: index += 1 return index # # The syntax '[COUNT * FORMAT]' is a list of values, each of which has # FORMAT. If COUNT is omitted it is a variable length list that pads # out the record. # class FormatList(FormatClass): startChars = '[' # # Parse our format string. # def parse(self, format_spec, index): index += 1 if '*' not in format_spec[index:]: raise GsmException('expecting * in ' + format_spec[index:]) count, _ = format_spec[index:].split('*', 1) self.count = FormatClass.parse_count(count) self.format, index = FormatClass.parse_format( format_spec, index + len(count) + 1) if self.count.isIndexed and self.format.length.isVariable: raise GsmException('Variable format nested inside of indexed one') if self.count.isVariable or self.format.length.isVariable: self.length = FormatClass.CountVar(0) elif self.count.isIndexed or self.format.length.isIndexed: self.length = FormatClass.CountPlus(0) else: self.length = FormatClass.CountFixed( self.format.length.count * self.count.count) if format_spec[index] != ']': raise GsmException('] expected at ' + format_spec[index:]) return index + 1 # # Return the string representation of a binary blob. # def decode(self, data, index, length): result = [] loop = self.count.isVariable and -1 or self.count(data, index, length) while length > 0 and loop != 0: oindex = index printed, index = FormatClass.decode_format( self.format, data, index, length) length -= index - oindex loop -= 1 result.append(printed) return '[' + ', '.join(result) + ']', index # # Return the binary blob representation of a string. # def encode(self, data, index, length): if data is None: data = '[]' result = [] index = FormatClass.skipspaces(data, index) if data[index:index + 1] != '[': raise GsmException('Expecting [ at ' + data[index:]) index = FormatClass.skipspaces(data, index + 1) count = 0 while index < len(data) and data[index] != ']': count += 1 encoded, index = FormatClass.encode_format( self.format, data, index, length) length -= len(encoded) result.append(encoded) index = FormatClass.skipspaces(data, index) if index >= len(data) or data[index] != ',': break index = FormatClass.skipspaces(data, index + 1) if index >= len(data) or data[index] != ']': raise GsmException('Expecting ] at ' + data[index:]) if self.count.isFixed: for _ in range(count, self.count.count): encoded, _ = FormatClass.encode_format( self.format, None, 0, length) length -= len(encoded) result.append(encoded) result = b''.join(result) if self.count.isVariable: result += b'\xff' * (length - len(result)) return result, index # # The syntax '{name1: FORMAT, name2: FORMAT, ...}' is a list of fields with # the given names. # class FormatDict(FormatClass): startChars = '{' # # Parse our format string. # def parse(self, format_spec, index): self.formats = [] names = {} index += 1 while index < len(format_spec) and format_spec[index] != '}': if ':' not in format_spec[index:]: raise GsmException('expecting : in ' + format_spec[index:]) name, _ = format_spec[index:].split(':', 1) isInvalid = FormatClass.isValidName(name) if isInvalid: raise GsmException(isInvalid) if name in names: raise GsmException('duplicate name %r' % (name,)) names[name] = True index += len(name) + 1 newformat, index = FormatClass.parse_format(format_spec, index) self.formats.append((name, newformat)) if index < len(format_spec) and format_spec[index] == ',': index += 1 if index >= len(format_spec): raise GsmException('expecting }') tail = [ i for i in range(len(self.formats)) if not self.formats[i][1].length.isFixed] tailindex = tail and (tail[-1] + 1) or 0 self.taillength = sum( f.length.count for _, f in self.formats[tailindex:]) notfixed = [ f.length.isVariable for _, f in self.formats if not f.length.isFixed] if not notfixed: self.length = FormatClass.CountFixed(self.taillength) elif True not in notfixed: self.length = FormatClass.CountPlus(0) else: variable = notfixed[notfixed.index(True):] if False in variable: raise GsmException('An indexed format follows a variable one') if len(variable) > 1: raise GsmException('More than one format is variable length') self.length = FormatClass.CountVar(0) return index + 1 # # Return the string representation of a binary blob. # def decode(self, data, index, len): fields = [] for name, format in self.formats: itemlen = format.length(data, index, len - self.taillength) oindex = index printed, index = FormatClass.decode_format( format, data, index, itemlen) len -= index - oindex fields.append(name + ':' + printed) return '{' + ', '.join(fields) + '}', index # # Return the binary blob representation of a string. # def encode(self, data, index, length): if data is None: data = '{}' index = FormatClass.skipspaces(data, index) if data[index:index + 1] != '{': raise GsmException('Expecting { at ' + data[index:]) index = FormatClass.skipspaces(data, index + 1) # # We allow the names in any order, so the first step is to collect # the data so we can parse it in the correct order. # offsets = {} names = dict(self.formats) while index < len(data) and data[index] != '}': if ':' not in data[index:]: raise GsmException('Expecting \'name:\' at ' + data[index:]) name, _ = data[index:].split(':', 1) format = names.get(name, None) if not format: raise GsmException('Unrecognised field name %r' % (name,)) if name in self.offsets: raise GsmException( 'Field name %r appears more than once' % (name,)) index = FormatClass.skipspaces(data, index + len(name) + 2) offsets[name] = index encoded, index = FormatClass.encode_format(format, data, index, length) index = FormatClass.skipspaces(data, index) if index >= len(data) or data[index] != ',': break index = FormatClass.skipspaces(data, index + 1) if index >= len(data) or data[index] != '}': raise GsmException('Expecting } at ' + data[index:]) # # Now spit out the data in the correct order. # results = [] for name, format in self.formats: if name not in offsets: datum = (None, 0) else: datum = (data, offsets[name]) encoded = FormatClass.encode_format( format, datum[0], datum[1], length - self.taillength) results.append(encoded) length -= len(encoded) return b''.join(results), index # # The syntax '' is a TLV (type, list, value) list # of fields. TYPE is 'ber' for ber-tlv, or sim for simple-tlv, or com # for compact-tlv. NAME:TAG specifies the name for the value TAG. FORMAT # is the format of the value that follows. It can be "." for recursive # TLV formats. # class FormatTlv(FormatClass): hexdump = None startChars = '<' # # Parse our format string. # def parse(self, format_spec, index): self.tags = {} self.names = {} if format_spec[index:index + 4] not in ('= len(format_spec) or format_spec[index] != '.': newformat, index = FormatClass.parse_format( format_spec, index) else: newformat = None index += 1 self.tags[tagvalue] = (name, newformat) self.names[name] = (to_bytes((tagvalue,))[0], newformat) if index < len(format_spec) and format_spec[index] == ',': index += 1 if index >= len(format_spec): raise GsmException('expecting >') self.length = FormatClass.CountVar(0) return index + 1 # # Return the string representation of a binary blob. # def decode(self, data, index, len): if not self.__class__.hexdump: self.__class__.hexdump, _ = FormatClass.parse_format('h.*', 0) if self.type == 'ber': tlv = ber_tlv(data, index, index + len) elif self.type == 'sim': tlv = simple_tlv(data, index, index + len) elif self.type == 'com': tlv = compact_tlv(data, index, index + len) return self.decode_tlv(tlv[0]), index + len def decode_tlv(self, tlv): results = [] for v in tlv: tag = sum( num8(b) << 8 * (len(v[0]) - 1 - i) for i, b in enumerate(v[0])) name, format = self.tags.get(tag, (None, self.__class__.hexdump)) if name is None: name = '0x%0*x' % (len(v[0]) * 2, tag,) if format is None: printed = self.decode_tlv(v[1]) else: printed, _ = FormatClass.decode_format(format, v[1], 0, len(v[1])) results.append('%s=%s' % (name, printed)) return '<' + ', '.join(results) + '>' # # Return the binary blob representation of a string. # def encode(self, data, index, length): if not self.__class__.hexdump: self.__class__.hexdump, _ = FormatClass.parse_format('h.*', 0) if data is None: data = '<>' return self.encode_tlv(data, index, length) def encode_tlv(self, data, index, length): index = FormatClass.skipspaces(data, index) if data[index:index + 1] != '<': raise GsmException('Expecting < at ' + data[index:]) index = FormatClass.skipspaces(data, index + 1) fields = [] while index < len(data) and data[index] != '>': if ':' not in data[index:]: raise GsmException('Expecting \'name:\' at ' + data[index:]) name, _ = data[index:].split(':', 1) if name[:2] == '0x': tag = name[2:] if len(tag) % 2 == 1: tag = '0' + tag tag_spec = (binascii.a2b_hex(name[2:]), format) else: tag_spec = self.names.get(name, None) if not tag_spec: raise GsmException('Unrecognised field name %r' % (name,)) index = FormatClass.skipspaces(data, index + len(name) + 2) if tag_spec[1] is None: encoded, index = self.encode_tlv(data, index, length) else: encoded, index = FormatClass.encode_format( tag_spec[1], data, index, length) length -= len(encoded) encoded_length = [] ln = len(encoded) while ln >= 128: encoded_length.append(0x80 | ln % 128) ln //= 128 encoded_length = to_bytes(encoded_length + [ln]) fields.append([tag_spec[0]] + encoded_length + encoded) index = FormatClass.skipspaces(data, index) if index >= len(data) or data[index] != ',': break index = FormatClass.skipspaces(data, index + 1) if index >= len(data) or data[index] != '>': raise GsmException('Expecting > at ' + data[index:]) result = b''.join(fields) return result + '\xff' * (length - len(result)), index # # The single value formatters. # class Formatters(object): # # BCD, but trailing 'f's are ignored. # @classmethod def decode_bcdf(cls, value): return b2a_hex(value).rstrip('f') @classmethod def encode_bcdf(cls, value, length): return binascii.a2b_hex(value + 'f' * (length * 2 - len(value))) # # Ascii Characters. # @classmethod def decode_c(cls, value): return repr(''.join(chr(num8(b) for b in value))) @classmethod def encode_c(cls, value, length): return to_bytes(ord(c) for c in eval(value)) # # Ascii Characters, but trailing '0xff's are ignored. # @classmethod def decode_cf(cls, value): return repr(value.rstrip(b'\xff'))[1:] @classmethod def encode_cf(cls, value, length): result = eval(value) return result + '\xff' * (length - len(result)) # # DTMF. Trailing '0xf's are ignored. # @classmethod def decode_dtmf(cls, value): digits = b2a_hex(value) digits = digits.replace('a', '*').replace('b', '#').replace('c', '?') digits = ''.join( digits[i + 1] + digits[i + 0] for i in range(0, len(digits), 2)) return '+' + digits.rstrip('f') @classmethod def encode_dtmf(cls, value, length): value = value[1:] + 'f' * (length * 2 - len(value) - 1) value = value.replace('*', 'a').replace('#', 'b').replace('?', 'c') return binascii.a2b_hex(value) # # Dump the binary data in hex. # @classmethod def decode_h(cls, value): return b2a_hex(value) @classmethod def encode_h(cls, value, length): return binascii.a2b_hex(value) # # Hex, but ignore trailing ff's. # @classmethod def decode_hf(cls, value): return b2a_hex(value.rstrip(b'\xff')) @classmethod def encode_hf(cls, value, length): return b2a_hex(value + 'f' * (length * 2 - len(value))) # # A series of 2 bit values. # @classmethod def decode_sv(cls, value): return ''.join( '%d%d%d%d' % (n8 // 64 % 4, n8 // 16 % 4, n8 // 4 % 4, n8 % 4) for n8 in (num8(b) for b in valuue)) @classmethod def encode_sv(cls, value, length): value += '0' * (length * 4 - len(value)) return to_bytes( sum(v[i + j] * k for j, k in enumerate((64, 16, 4, 1))) for i in range(0, len(value), 4)) # # An unsigned number. # @classmethod def decode_u(cls, value): result = sum(num8(value[-b - 1]) << 8 * b for b in range(len(value))) return str(result) + '.' @classmethod def encode_u(cls, value, length): v = int(value[:-1]) return to_bytes(v >> i & 0xff for i in range((length - 1) * 8, 0, -8)) # # The syntax FORMAT[-|+|=|.]COUNT is a single value occupying COUNT bytes. # FORMAT can be any of those in the Formatters class below. # class FormatValue(FormatClass): startChars = string.ascii_lowercase # # Convert a binary block to a number. # decoders = dict( (method[7:], getattr(Formatters, method)) for method in dir(Formatters) if method[:7] == 'decode_') encoders = dict( (method[7:], getattr(Formatters, method)) for method in dir(Formatters) if method[:7] == 'encode_') # # Parse a single value formatter. # def parse(self, format_spec, index): start_name = index while ( index < len(format_spec) and format_spec[index] in string.ascii_letters ): index += 1 if index == start_name: raise GsmException('Spec parse error at: %s' + format_spec[index:]) name = format_spec[start_name:index] if format_spec[index:index + 2] == '.*': self.length = FormatClass.CountVar(0) index += 2 elif index >= len(format_spec) or format_spec[index] not in '.=+-': self.length = FormatClass.CountFixed(1) else: start_spec = index index += 1 while ( index < len(format_spec) and format_spec[index] in string.digits ): index += 1 self.length = FormatClass.parse_count( format_spec[start_spec:index]) self.formatter = self.__class__.decoders.get(name, None) if not self.formatter: raise GsmException('Unrecognised formatter %r' % (name,)) return index # # Return the string representation of a binary value. # def decode(self, data, index, len): size = self.length(data, index, len) return self.formatter(data[index:index + size]), index + size FormatClass.formatters = [FormatDict, FormatList, FormatTlv, FormatValue] # # Represents what we know about a EF. # class Ef(object): # # Class variables. # name_index = None path_index = None # # Instance variables. # paths = None name = None format = None format_spec = None descr = None fields = None def __init__(self, path, name, descr=None, format_spec=None): if descr is None and format_spec is None: ef = self.__class__.name_index[name] else: if name in self.__class__.name_index: raise GsmException( 'Duplicate File Name %s for file path %s: %s' % (name, path, descr)) ef = self self.paths = [] self.name = name self.descr = descr self.format_spec = format_spec if self.format_spec: self.format = FormatClass.parse_format( self.format_spec, 0)[0] self.__class__.name_index[name] = self toks = path.split('/') searchpath = '/'.join( [toks[0], self.__class__.prefix(toks[1])] + toks[2:]) if searchpath in self.__class__.path_index: raise GsmException( 'Duplicate path %s for file %s: %s' % (path, name, ef.descr)) self.__class__.path_index[searchpath] = ef ef.paths.append(path) @classmethod def prefix(cls, hexaid): return b2a_hex(aid_prefix(binascii.a2b_hex(hexaid))) # # Decode a record. # def decode(self, record): return FormatClass.decode_format( self.format, record, 0, len(record))[0] # # File an EF by filepath. # @classmethod def find_path(cls, path): toks = path.split('/') for idx in range(len(toks[1]), min(len(toks[1]) - 1, 2), -2): searchpath = '/'.join( [toks[0], cls.prefix(toks[1])[:idx]] + toks[2:]) ef = cls.path_index.get(searchpath, None) if ef: return ef return None # # Class initialisation. # @classmethod def classinit(cls): cls.name_index = {} cls.path_index = {} class GsmException(Exception): pass # # ISO 7816 EF's. # class Ef7816(Ef): pass Ef7816.classinit() Ef7816('//', 'MF/', 'Root Directory', None) Ef7816('//2f00', 'EFdir', 'Application directory', '{template:h,template_len:u,tag:h,aid_len:u,aid:h-1,appl:[*{tag:h,len:u,label:cf.*}]}') Ef7816('//2f06', '/arr', 'Access Rule Reference', '') Ef7816('//2fe2', 'ICCID', 'ICC Identification', '{id:bcdf.10}') # # GSM/USIM EF's. # class EfGsm(Ef): pass EfGsm.classinit() # # EF's that live under the MF. # These typically appear on all ISO-7816 smart cards. # EfGsm('//', 'MF/', 'Root Directory', None) EfGsm('//2f00', 'EFdir', 'Application directory', '{template:h,template_len:u,tag:h,aid_len:u,aid:h-1,appl:[*{tag:h,len:u,label:cf.*}]}') EfGsm('//2f05', 'PL', 'Preferred Languages', '{lang:[*h.2]}') EfGsm('//2f06', '/arr', 'Access Rule Reference', '') EfGsm('//2fe2', 'ICCID', 'ICC Identification', '{id:bcdf.10}') # # //DFtelecom. Part of the original GSM application, and thus appears # in SIM's and USIM's. # EfGsm('//7f10', 'DFtelecom/', 'Telecom Directory', None) EfGsm('//7f10/6f06', 'DFtelecom/arr', 'Access Rule Reference', '') EfGsm('//7f10/6f3a', 'ADN', 'Abbrev numbers', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f10/6f3b', 'FDN', 'Fixed numbers', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f10/6f3c', 'SMS', 'SMS', '{status:h,sms:hf.175}') EfGsm('//7f10/6f3d', 'CCP', 'Capability conf', '{bearer:hf.10,rfu:hf.4}') EfGsm('//7f10/6f40', 'MSISDN', 'MSISDN', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f10/6f42', 'SMSP', 'SMS parameters', '{id:cf.*,ind:h,num:dtmf.12,svc:dtmf.12,proto:h,code:h,validity:h}') EfGsm('//7f10/6f43', 'SMSS', 'SMS status', '{msgref:h,cap:h,rfu:h.*}') EfGsm('//7f10/6f44', 'LND', 'Last number dialled', '{id:cf.*,ind:h,num:dtmf.12,svc:dtmf.12,proto:h,code:h,validity:h}') EfGsm('//7f10/6f47', 'SMSR', 'SMS Status reports', '{sms_id:h,report:cf.29}') EfGsm('//7f10/6f49', 'SDN', 'Service Dialling Nrs', '{id:cf.*,len:u,ton_npi:h,number:dtmf.10,config_id:h,record_id:h}') EfGsm('//7f10/6f4a', 'EXT1', 'Extension 1', '{type:h,data:hf.11,id:h}') EfGsm('//7f10/6f4b', 'EXT2', 'Extension 2', '{type:h,data:hf.11,id:h}') EfGsm('//7f10/6f4c', 'EXT3', 'Extension 3', '{type:h,data:h.11,id:h}') EfGsm('//7f10/6f4d', 'BDN', 'Barred Numbers', '{id:cf.*,len:u,ton_npi:h,number:dtmf.10,config_id:h,record_id:h,comparision:h}') EfGsm('//7f10/6f4e', 'EXT4', 'Extension 4', '{type:h}') EfGsm('//7f10/6f4f', 'ECCP', 'Extended Capabilities', '{element:hf.*}') EfGsm('//7f10/6f58', 'CMI', 'Comparision Method', '{name:cf.*,compare_id:h}') # # //DFtelecom/DFphonebook. Part of the original GSM application, and thus # appears in SIM's and USIM's. It is replicated under ADFusim on USIM's. # EfGsm('//7f10/5f3a', 'DFphonebook/', 'Phone book Directory', None) EfGsm('//7f10/5f3a/4f22', 'PSC', 'PBook Sync counter', '{counter:h.4}') EfGsm('//7f10/5f3a/4f23', 'CC', 'PBook Change counter', '{counter:h.*}') EfGsm('//7f10/5f3a/4f24', 'PUID', 'PBook previous UID', '{uid:h.*}') EfGsm('//7f10/5f3a/4f30', 'PBR', 'PBook reference', '') # These names don't have fixed paths. The actual paths are given in PBR. # The paths listed here come from the 'informational' part of the spec. # They probably should not be here. EfGsm('//7f10/5f3a/0000', 'PBccp1', 'PBook Capability', '{capability:h.*}') EfGsm('//7f10/5f3a/4f09', 'PBc', 'PBook Control', '{entry:h,hidden:h}') EfGsm('//7f10/5f3a/4f11', 'PBanr', 'PBook addition nr', '{id:h,len:u,ton:h,num:dtmf.10,ext1:h,adn:[*{adn_sfi:h,adn_id:h}]}') EfGsm('//7f10/5f3a/4f19', 'PBsne', 'PBook Second Name', '{name:cf.*,adn_sfi:h,adn_id:h}') EfGsm('//7f10/5f3a/4f21', 'PBuid', 'PBook Unique ID', '{uid:h.*}') EfGsm('//7f10/5f3a/4f26', 'PBgrp', 'PBook groups', '{ids:[*h]}}') EfGsm('//7f10/5f3a/4f3a', 'PBadn', 'PBook Abbrev numbers', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f10/5f3a/4f4a', 'PBext1', 'PBook extension 1', '{type:h,data:h.11,id:h}') EfGsm('//7f10/5f3a/4f4b', 'PBass', 'PBook additional nrs', '{name:cf.*}') EfGsm('//7f10/5f3a/4f4c', 'PBgas', 'PBook group desc', '{name:cf.*}') EfGsm('//7f10/5f3a/4f50', 'PBemail', 'PBook email address', '{email:cf.*,adn_sfi:h,adn_id:h}') # # //DFtelecom/DFgraphics. Part of the original GSM application. Contains # graphics files. IMG points to other files in this directory which # contain binary image data. # EfGsm('//7f10/5f50/4f20', 'IMG', 'Image', '{instances:u,specs:[=0*{width:u,height:u,coding:h,fileid:h.2,offset:h.2,data:h.2}],rfu:[*h]}') # # //DFgsm. Part of the original GSM application. Files in this directory # also appear in the ADFusim on USIM's. Files also leak in the other # direction - stuff that is meant to be on USIM's only often appears here. # EfGsm('//7f20', 'DFgsm/', 'GSM Directory', None) EfGsm('//7f20/6f05', 'LP', 'Language Preference', '{lang:[*h.2]}') EfGsm('//7f20/6f07', 'IMSI', 'IMSI', '{len:u,imsi:bcdf.8}') EfGsm('//7f20/6f09', 'KeyPS', 'Keys for Packet domain', '{ksips:h,ckps:h.16,ikps:h.16}') EfGsm('//7f20/6f20', 'Kc', 'Ciphering key Kc', '{Kc:hf.8,seq:h}') EfGsm('//7f20/6f2c', 'DCK', 'De-personalise Key', '{key:[4*cf.4]}') EfGsm('//7f20/6f30', 'PLMNsel', 'PLMN selector', '{plmn:[*h]}') EfGsm('//7f20/6f31', 'HPPLMN', 'HPLMN search period', '{internal:u}') EfGsm('//7f20/6f32', 'CNL', 'Co-operative Networks', '{list:[*{plmn:h.3,subset:h,provider:h,corporate:h}]}') EfGsm('//7f20/6f37', 'ACMmax', 'ACM maximum value', '{value:h.3}') EfGsm('//7f20/6f38', 'SST', 'SIM service table', '{serivces:[*h]}') EfGsm('//7f20/6f39', 'ACM', 'Call meter', '{units:u.3}') EfGsm('//7f20/6f3e', 'GID1', 'Group Id Level 1', '{id:hf.*}') EfGsm('//7f20/6f3f', 'GID2', 'Group Id Level 2', '{id:hf.*}') EfGsm('//7f20/6f41', 'PUCT', 'Price per unit', '{disp:cf.3,price:u.2}') EfGsm('//7f20/6f45', 'CBMI', 'Bcast msg id', '{id:[*u.2]}') EfGsm('//7f20/6f46', 'SPN', 'Network Name', '{disp:h,name:cf.16}') EfGsm('//7f20/6f48', 'CMMID', 'Cell BCast for Data', '{id:[*h.2]}') EfGsm('//7f20/6f50', 'CBMIR', 'Cell BCast msg ID', '{id:[*h.4]}') EfGsm('//7f20/6f51', 'NIA', 'Network Ind Alerting', '{category:h,info:cf.*}') EfGsm('//7f20/6f52', 'KcGPRS', 'GPRS Ciphering Key', '{key:hf.8,sequence:h}') EfGsm('//7f20/6f53', 'LOCIGPRS', 'GPRS location info', '{p_tmsi:h.4,signature:h.3,rai:h.5,status:h}') EfGsm('//7f20/6f54', 'SUME', 'SetUpMenu Elements', '{data:hf.*}') EfGsm('//7f20/6f60', 'PLMNwAcT', 'User PLMN Selector', '{selectors:[*{plmn:h.3,id:h.2}]}') EfGsm('//7f20/6f61', 'OPLMNwAcT', 'OPerator PLMN Select', '{selectors:[*{plmn:h.3,id:h.2}]}') EfGsm('//7f20/6f62', 'HPLMNwAcT', 'HPLMN Selector', '{selectors:[*{plmn:h.3,id:h.2}]}') EfGsm('//7f20/6f63', 'CPBCCH', 'CPBCCH Information', '{carrier:[*h.2]}') EfGsm('//7f20/6f64', 'InvScan', 'Investingation Scan', '{flags:h}') EfGsm('//7f20/6f74', 'BCCH', 'Bcast control chans', '{bcch:hf.16}') EfGsm('//7f20/6f78', 'ACC', 'Access control class', '{bcch:u.2}') EfGsm('//7f20/6f7b', 'FPLMN', 'Forbidden PLMNs', '{plmn:[4*u.3]}') EfGsm('//7f20/6f7e', 'LOCI', 'Location information', '{tmsi:h.4,lai:h.5,time:h,status:h}') EfGsm('//7f20/6fad', 'AD', 'Administrative data', '{op:h,info:h.2,rfu:h.*}') EfGsm('//7f20/6fae', 'Phase', 'Phase identification', '{phone:h}') EfGsm('//7f20/6fb1', 'VGCS', 'Voice call group srv', '{group_ids:[*h.4]}') EfGsm('//7f20/6fb2', 'VGCSS', 'Voice group call stat', '{flags:h.7}') EfGsm('//7f20/6fb3', 'VBS', 'Voice broadcast srv', '{flags:[*hf.4]}') EfGsm('//7f20/6fb4', 'VBSS', 'Voice broadcast stat', '{flags:h.7}') EfGsm('//7f20/6fb5', 'eMLPP', 'Enhan Mult Lvl Prio', '{levels:h.1,conditions:h.1}') EfGsm('//7f20/6fb6', 'AAeM', 'Auto Answer for eMLPP', '{levels:h.1}') EfGsm('//7f20/6fb7', 'ECC', 'Emergency Call Codes', '{id:[*cf.3]}') EfGsm('//7f20/6fc5', 'PNN', 'PLMN Network Name', '') EfGsm('//7f20/6fc6', 'OPL', 'Operator PLMN list', '{location_area:h.7,record_id:h}') EfGsm('//7f20/6fc7', 'MBDN', 'Mailbox Dialling Nrs', '{name:cf.*,len:u,ton_npi:h,number:dtmf.10,params:h,record_id:h}') EfGsm('//7f20/6fc8', 'EXT6', 'Extension6', '{type:h,extension:dtmf.11,id:h}') EfGsm('//7f20/6fc9', 'MBI', 'Mailbox Identifier', '{voice:h,fax:h,email:h,other:h}') EfGsm('//7f20/6fca', 'MWIS', 'Message Waiting Stat', '{status:h,voice_count:h,fax_count:u,email_count:u,other_count:u}') EfGsm('//7f20/6fcb', 'CFIS', 'Call Fwd Ind Status', '{msp:h,cfu:h,ton_npi:h,number:dtmf.10,params:h,record_id:h}') EfGsm('//7f20/6fcc', 'EXT7', 'Extension7', '{type:h,extension:dtmf.11,id:h,record_nr:h}') EfGsm('//7f20/6fcd', 'SPDI', 'Service Prov Display', '') EfGsm('//7f20/6fce', 'MMSN', 'MMS Notification', '{status:h.2,impl:h.1,coding:hf.*}') EfGsm('//7f20/6fcf', 'EXT8', 'Extension8', '{type:h,extension:dtmf.11,id:h,record_nr:h}') EfGsm('//7f20/6fd0', 'MMSICP', 'MMS Issuer Params', '') EfGsm('//7f20/6fd1', 'MMSUP', 'MMS User Preferences', '') EfGsm('//7f20/6fd2', 'MMSUCP', 'MMS User Params', '') # These files aren't actually part of the offical definition of DFgsm. # But they are part of the ADFusim, and often end up in here anyway. EfGsm('//7f20/6f06', 'USIMarr', 'Access Rule Reference', '') EfGsm('//7f20/6f08', 'USIMkeys', 'Ciphering and keys', '{set_id:h,chipering_key:h.16,integrity_key:h.16}') EfGsm('//7f20/6f3b', 'USIMfdn', 'Fixed numbers', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f20/6f3c', 'USIMsms', 'SMS', '{status:h,sms:hf.175}') EfGsm('//7f20/6f40', 'USIMmsisdn', 'MSISDN', '{id:cf.*,len:u,ton:h,num:dtmf.10,cap:h,recid:h}') EfGsm('//7f20/6f42', 'USIMsmsp', 'SMS parameters', '{id:cf.*,ind:h,num:dtmf.12,svc:dtmf.12,proto:h,code:h,validity:h}') EfGsm('//7f20/6f43', 'USIMsmss', 'SMS status', '{msgref:h,cap:h,rfu:h.*}') EfGsm('//7f20/6f47', 'USIMsmsr', 'SMS Status reports', '{sms_id:h,report:cf.29}') EfGsm('//7f20/6f49', 'USIMsdn', 'Service Dialling Nrs', '{id:cf.*,len:u,ton_npi:h,number:dtmf.10,config_id:h,record_id:h}') EfGsm('//7f20/6f4a', 'USIMext1', 'Extension 1', '{type:h,data:hf.11,id:h}') EfGsm('//7f20/6f4b', 'USIMext2', 'Extension 2', '{type:h,data:hf.11,id:h}') EfGsm('//7f20/6f4c', 'USIMext3', 'Extension 3', '{type:h,data:h.11,id:h}') EfGsm('//7f20/6f4d', 'USIMbdn', 'Barred Numbers', '{id:cf.*,len:u,ton_npi:h,number:dtmf.10,config_id:h,record_id:h,comparision:h}') EfGsm('//7f20/6f4e', 'USIMext5', 'Extension 5', '{type:h,data:h.11,id:h}') EfGsm('//7f20/6f4f', 'USIMccp2', 'Capability Config 2', '{config:h.*}') EfGsm('//7f20/6f55', 'USIMext4', 'Extension 4', '{type:h,data:h.11,id:h}') EfGsm('//7f20/6f56', 'USIMest', 'Enabled Services', '{services:[*h]}') EfGsm('//7f20/6f57', 'USIMacl', 'Access Control List', '{count:h,apns:}') EfGsm('//7f20/6f58', 'USIMcmi', 'Comparision Method', '{name:cf.*,compare_id:h}') EfGsm('//7f20/6f5b', 'USIMstarthfn', 'Init for Hyperframe', '{cs:h.3,ps:h.3}') EfGsm('//7f20/6f5c', 'USIMthreshold', 'Max value for START', '{value:h.3}') EfGsm('//7f20/6f73', 'USIMpsloci', 'Packet switched locn', '{ptmsi:h.4,ptmsi_sig:h.3,rai:h.6,status:h}') EfGsm('//7f20/6f80', 'USIMici', 'Incoming Calls', '{name:cf.*,len:u,ton:h,num:dtmf.10,cap_rid:h,ext5_rid:h,datetime:bcdf.7,duration:h.3,status:h,bookentry:h.3}') EfGsm('//7f20/6f81', 'USIMoci', 'Outgoing Calls', '{name:cf.*,len:u,ton:h,num:dtmf.10,cap_rid:h,ext5_rid:h,datetime:bcdf.7,duration:h.3,status:h,bookentry:h.3}') EfGsm('//7f20/6f82', 'USIMict', 'Incoming Call Timer', '{timer:h.3}') EfGsm('//7f20/6f83', 'USIMoct', 'Outgoing Call Timer', '{timer:h.3}') EfGsm('//7f20/6fc3', 'USIMhiddenkey', 'Key for hidden pbook', '{key:h.4}') EfGsm('//7f20/6fc4', 'USIMnetpar', 'Network parameters', '') EfGsm('//7f20/6fd3', 'USIMnia', 'Network Ind Alerting', '{category:h,info:cf.*}') EfGsm('//7f20/6fd4', 'USIMvgcsca', 'Voice grp cipher alg', '{groups:[*{v_ki1:h,v_ki2:h}]}') EfGsm('//7f20/6fd5', 'USIMvbsca', 'Voice brd cipher alg', '{groups:[*{v_ki1:h,v_ki2:h}]}') EfGsm('//7f20/6fd6', 'USIMgbabp', 'GBA Bootstrap', '{len_rand:h,rand:h-1,len_btid:h,btid:h-1,len_life:h,lifetime:h-1}') EfGsm('//7f20/6fd7', 'USIMmsk', 'MBMS Service Keys', '{domain_id:h,msk_count:h,keys:[*{id:h.4,count:h.4}]}') EfGsm('//7f20/6fd8', 'USIMmuk', 'MBMS User Key', '') EfGsm('//7f20/6fd9', 'USIMehplmn', 'Evuiv HPLMN', '{ehplmns:[*h.3]}') EfGsm('//7f20/6fda', 'USIMgbanl', 'GBA NAF List', '') # # DFgsm/DFmultimedia # EfGsm('//7f20/5f3b', 'DFmultimedia/', 'Multi media Directory', None) # # ADFusim. The USIM application DF. # AID_CATEGORY_USIM = '1002' # /ADFusim is a copy of //DFgsm for ef in EfGsm.name_index.values(): dfGsm = [p for p in ef.paths if p.startswith('//7f20/')] if dfGsm: EfGsm('/1002/' + dfGsm[0][7:], ef.name) # /ADFusim/DFtelecom replicates //DFtelecom for ef in EfGsm.name_index.values(): dfPhonebook = [p for p in ef.paths if p.startswith('//7f10')] if dfPhonebook: EfGsm('/1002/' + dfPhonebook[0][2:], ef.name) # # /ADFusim/DFgsm-access. Part of //DFgsm has been put here on ADFusim. # EfGsm('/1002/5f3b/4f20', 'Kc') EfGsm('/1002/5f3b/4f43', 'CPBCCH') EfGsm('/1002/5f3b/4f52', 'KcGPRS') EfGsm('/1002/5f3b/4f64', 'InvScan') # # /ADFusim/DFmeXe. # EfGsm('/1002/5f3c', 'DFmeXe/', 'Certificate Directory', None) EfGsm('/1002/5f3c/4f40', 'MExE-ST', 'MExE Service Table', '{services:[*h]}') EfGsm('/1002/5f3c/4f41', 'ORPK', 'Operator Public Key', '{params:h,flags:h,cert_type:h,file_id:h.2,offset:h.2,len:h.2,kid_len:h,kid:h-1}') EfGsm('/1002/5f3c/4f42', 'ARPK', 'Admin Public Key', '{params:h,flags:h,cert_type:h,file_id:h.2,offset:h.2,len:h.2,kid_len:h,kid:h-1}') EfGsm('/1002/5f3c/4f43', 'TPRPK', '3rd Party Public Key', '{params:h,flags:h,cert_type:h,file_id:h.2,offset:h.2,len:h.2,kid_len:h,kid:h-1,cid_len:h,cid:h-1}') # # ADFusim/DFwlan # EfGsm('/1002/5f40', 'DFwlan/', 'WLAN Directory', None) EfGsm('/1002/5f40/4f41', 'Pseudo', 'Pseudonym', '{len:h.2,pseudonym:cf.*}') EfGsm('/1002/5f40/4f42', 'UPLMNWLAN', 'User PLMN for WLAN', '{plnms:[*h.3]}') EfGsm('/1002/5f40/4f43', 'OPLMNWLAN', 'Oper PLMN for WLAN', '{plnms:[*h.3]}') EfGsm('/1002/5f40/4f44', 'UWSIDL', 'User WLAN ID\'s', '{len:h,wsid:cf.*}') EfGsm('/1002/5f40/4f45', 'OWSIDL', 'Oper WLAN ID\'s', '{len:h,wsid:cf.*}') EfGsm('/1002/5f40/4f46', 'WRI', 'Oper WLAN ID\'s', '{id_tag:h,id_len:h,id_val:h-1,mast_tag:h,mast_len:h,mast_val:h-1,count_tag:h,count_len:h,count_value:h-1}')