# -*- coding: UTF-8 -*- """ card: Library adapted to request (U)SIM cards and other types of telco cards. Copyright (C) 2010 Benoit Michau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ ################################# # generic functions # # being used in smartcard specs # ################################# from collections import deque from smartcard.util import toBytes ############### # log wrapper # ############### log_levels = {1:'ERR', 2:'WNG', 3:'DBG'} def log(level, string): # could output to a file # but here, just print() print('[%s] %s' % (log_levels[level], string)) # from python 2.6, format('b') allows to use 0b10010110 notation: # much convinient def byteToBit(byte): ''' byteToBit(0xAB) -> [1, 0, 1, 0, 1, 0, 1, 1] converts a byte integer value into a list of bits ''' bit = [0, 0, 0, 0, 0, 0, 0, 0] for i in range(8): if byte % pow(2, i+1): bit[7-i] = 1 byte = byte - pow(2, i) return bit # equivalent to the pyscard function "toASCIIBytes" # new version of python (>2.6) seems to have a built-in "bytes" type def stringToByte(string): ''' stringToByte('test') -> [116, 101, 115, 116] converts a string into a list of bytes ''' bytelist = [] for c in string: bytelist.extend( toBytes(c.encode('hex')) ) return bytelist # equivalent to the pyscard function "toASCIIString" def byteToString(bytelist): ''' byteToString([116, 101, 115, 116]) -> 'test' converts a list of bytes into a string ''' string = '' for b in bytelist: string += chr(b) return string def LV_parser(bytelist): ''' LV_parser([0x02, 0xAB, 0xCD, 0x01, 0x12, 0x34]) -> [[171, 205], [18], []] parses Length-Value records in a list of bytes returns a list of list of bytes length coded on 1 byte ''' values = [] while len(bytelist) > 0: l = bytelist[0] values.append( bytelist[1:1+l] ) bytelist = bytelist[1+l:] return values def first_TLV_parser(bytelist): ''' first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205]) parses first TLV format record in a list of bytelist returns a 3-Tuple: Tag, Length, Value Value is a list of bytes parsing of length is ETSI'style 101.220 ''' Tag = bytelist[0] if bytelist[1] == 0xFF: Len = bytelist[2]*256 + bytelist[3] Val = bytelist[4:4+Len] else: Len = bytelist[1] Val = bytelist[2:2+Len] return (Tag, Len, Val) def TLV_parser(bytelist): ''' TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...] loops on the input list of bytes with the "first_TLV_parser()" function returns a list of 3-Tuples ''' ret = [] while len(bytelist) > 0: T, L, V = first_TLV_parser(bytelist) if T == 0xFF: # padding bytes break ret.append( (T, L, V) ) # need to manage length of L if L > 0xFE: bytelist = bytelist[ L+4 : ] else: bytelist = bytelist[ L+2 : ] return ret def first_BERTLV_parser(bytelist): ''' first_BERTLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> ([1, 'contextual', 'constructed', 10], [1, 2], [171, 205]) parses first BER-TLV format record in a list of bytes returns a 3-Tuple: Tag, Length, Value Tag: [Tag class, Tag DO, Tag number] Length: [Length of length, Length value] Value: [Value bytes list] parsing of length is ETSI'style 101.220 ''' # Tag class and DO byte0 = byteToBit(bytelist[0]) if byte0[0:2] == [0, 0]: Tag_class = 'universal' elif byte0[0:2] == [0, 1]: Tag_class = 'applicative' elif byte0[0:2] == [1, 0]: Tag_class = 'contextual' elif byte0[0:2] == [1, 1]: Tag_class = 'private' if byte0[2:3] == [0]: Tag_DO = 'primitive' elif byte0[2:3] == [1]: Tag_DO = 'constructed' # Tag coded with more than 1 byte i = 0 if byte0[3:8] == [1, 1, 1, 1, 1]: Tag_bits = byteToBit(bytelist[1])[1:8] i += 1 while byteToBit(bytelist[i])[0] == 1: i += 1 Tag_bits += byteToBit(bytelist[i])[1:8] # Tag coded with 1 byte else: Tag_bits = byte0[3:8] # Tag number calculation Tag_num = 0 for j in range(len(Tag_bits)): Tag_num += Tag_bits[len(Tag_bits)-j-1] * pow(2, j) # Length coded with more than 1 byte (BER long form) if bytelist[i+1] & 0x80: Len_num = bytelist[i+1] - 0x80 Len = reduce(lambda x,y: (x<<8)+y, bytelist[i+2:i+2+Len_num]) Val = bytelist[i+2+Len_num:i+2+Len_num+Len] # Length coded with 1 byte else: Len_num = 1 Len = bytelist[i+1] Val = bytelist[i+2:i+2+Len] return ([i+1, Tag_class, Tag_DO, Tag_num], [Len_num, Len], Val) #return ([Tag_class, Tag_DO, Tag_num], Len, Val) def BERTLV_parser(bytelist): ''' BERTLV_parser([0xAA, ..., 0xFF]) -> [([T], L, [V]), ([T], L, [V]), ...] loops on the input bytes with the "first_BERTLV_parser()" function returns a list of 3-Tuples containing BERTLV records ''' ret = [] while len(bytelist) > 0: T, L, V = first_BERTLV_parser(bytelist) #if T == 0xFF: # break # padding bytes ret.append( (T[1:], L[1], V) ) # need to manage lengths of Tag and Length bytelist = bytelist[ T[0] + L[0] + L[1] : ] return ret def decode_BCD(data=[]): ''' decode_BCD([0x21, 0xFE, 0xA3]) -> '121415310' to decode serial number (IMSI, ICCID...) from list of bytes ''' string = '' for B in data: # 1st digit (4 LSB), can be padding (e.g. 0xF) if (B&0x0F) < 10: string += str(B&0x0F) # 2nd digit (4 MSB), can be padding (e.g. 0xF) if (B>>4) < 10: string += str(B>>4) if len(string) <= 0: return None else: return string def compute_luhn(digit_str=''): ''' compute_luhn('15632458') -> 4 return the luhn code of the digits provided ''' if not digit_str.isdigit(): print('you must provide a string of digits') return # append 0 d = [int(c) for c in digit_str+'0'] # sum of odd digits cs = sum(d[-1::-2]) # sum of (sum of digits(even digits * 2)) cs += sum([(v*2)%9 if v != 9 else 9 for v in d[-2::-2]]) # modulo 10: luhn checksum cs = cs%10 # return the luhn code if cs == 0: return cs else: return 10-cs def write_dict(dict, fd): ''' write a dict() content to a file descriptor ''' keys = dict.keys() keys.sort() fd.write('\n') for k in keys: rec = dict[k] if isinstance(rec, list) and \ len(rec) == [isinstance(i, int) for i in rec].count(True): rec = ''.join(['[', ', '.join(map(hex, rec)), ']']) fd.write('%s: %s\n' % (k, rec)) def make_graph(FS, master_name='(0x3F, 0x00)\nMF'): try: import pydot except: log(1, '(make_graph) pydot library not found: aborting') return #return # build a graph using graphviz Dot language, with pydot # create a graph with master MF or AID node nodes={} graph = pydot.Dot(graph_type='digraph', rankdir='LR') nodes['master'] = pydot.Node(master_name, style='filled', fillcolor='green') graph.add_node(nodes['master']) # attach nodes under master for file in FS: abspath = file['Absolut Path'] # build file name for the node label = ''.join(( \ ''.join((file['Name'], '\n')) if 'Name' in file.keys() else '', \ '(', hex(abspath[-2]), ' ', hex(abspath[-1]), ')')) # check for EF or DF for node color color='yellow' if file['Type'][:2] == 'EF' else 'blue' # make node nodes['%s'%abspath] = pydot.Node('%s'%abspath, label=label,\ style='filled', fillcolor=color) # put it in the graph and append it to the parent graph.add_node(nodes['%s'%abspath]) graph.add_edge(pydot.Edge(nodes['%s'%abspath[:-2]] if \ len(abspath) >= 4 else nodes['master'], \ nodes['%s'%abspath])) # and return graph ... return graph ####################################################### # Generic class to keep track of sent / received APDU # ####################################################### class apdu_stack: ''' input / output wrapping class for APDU communications allows to keep track of communications and exchanged commands based on the python "deque" fifo-like object ''' def __init__(self, limit=10): ''' initializes apdu_stack with the maximum of IO to keep track of ''' self.apdu_stack = deque([], limit) def push(self, apdu_response): ''' stacks the returned response into the apdu_stack ''' self.apdu_stack.append( apdu_response ) def __repr__(self): ''' represents the whole stack of responses pushed on ''' s = '' for apdu in self.apdu_stack: s += apdu.__repr__() + '\n' return s def __call__(self): ''' calling the apdu_stack returns the last response pushed on it ''' try: return self.apdu_stack[-1] except IndexError: return None