# # Copyright (C) 2009 Frank Morgner # # This file is part of virtualsmartcard. # # virtualsmartcard 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 3 of the License, or (at your option) any # later version. # # virtualsmartcard 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 # virtualsmartcard. If not, see . # """ @todo use bertlv_pack for fdm @todo zu lange daten abschneiden und trotzdem tlv laenge beibehalten """ from pickle import dumps, loads from ConstantDefinitions import * from TLVutils import * from SWutils import * from utils import stringtoint, inttostring def isEqual(list): # {{{ """Returns True, if all items are equal, otherwise False""" if len(list) > 1: for item in list: if item != list[0]: return False return True # }}} def walk(start, path): # {{{ """Walks a path of fids and returns the last file (EF or DF). start -- DF, where to look for the first fid path -- a string of fids """ if len(path) % 2 != 0: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) index = 0 while index < len(path): if not isinstance(start, DF): # File or application not found raise SwError(SW["ERR_FILENOTFOUND"]) start = start.select('fid', stringtoint(path[:2])) index = index + 2 return start # }}} def getfile_byrefdataobj(mf, refdataobjs): # {{{ """Returns a list of files according to the given list of reference data objects. mf -- the MF refdataobjs -- a list of 3-tuples (tag, length, newvalue)""" result = [None] for tag, length, newvalue in refdataobjs: if tag != TAG["FILE_REFERENCE"]: raise ValueError if length == 0: file = mf elif length <= 2: newvalue = stringtoint(newvalue) if length == 1: if newvalue & 5 != 0 or (newvalue >> 3) == 0 or (newvalue>>3) == (0xff>>3): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) else: file = mf.select('shortfid', newvalue >> 3) else: # length == 2: file = mf.select('fid', newvalue) else: if length % 2 == 1: raise NotImplementedError else: if newvalue[:2] == "\x3f\x00": # absolute file = walk(mf, newvalue[2:]) else: # relative file = walk(mf.currentDF(), newvalue[2:]) result.append(file) return result # }}} def write(old, newlist, offsets, datacoding, maxsize=None): # {{{ """Returns the status bytes and the result of a writeoperation according to the given data coding. @param old: string of old data @param newlist: a list of new data string @param offsets: a list of offsets, each for one new data strings @param datacoding: DCB["ONETIMEWRITE"] (replace) or DCB["WRITEOR"] (logical or) or DCB["WRITEAND"] (logical and) or DCB["PROPRIETARY"] (logical xor) @param maxsize: the maximum number of bytes in the result """ result = old listindex = 0 while listindex < len(offsets) and listindex < len(newlist): offset = offsets[listindex] new = newlist[listindex] writenow = len(new) if offset > len(result): raise SwError(SW["ERR_OFFSETOUTOFFILE"]) if maxsize and offset + writenow > maxsize: raise SwError(SW["ERR_NOTENOUGHMEMORY"], old) if datacoding == DCB["ONETIMEWRITE"]: result = (result[ 0 : offset ] + new[ 0 : writenow ] + result[ offset+writenow : len(result) ]) else: if offset + writenow > len(old): raise SwError(SW["ERR_NOTENOUGHMEMORY"], old) newindex = 0 resultindex = offset + newindex while newindex < writenow: if datacoding == DCB["WRITEOR"]: newpiece = chr( ord(result[resultindex]) | ord(new[newindex])) elif datacoding == DCB["WRITEAND"]: newpiece = chr( ord(result[resultindex]) & ord(new[newindex])) elif datacoding == DCB["PROPRIETARY"]: # we use it for XOR newpiece = chr( ord(result[resultindex]) ^ ord(new[newindex])) result = (result[0:resultindex] + newpiece + result[resultindex+1:len(result)]) newindex = newindex + 1 resultindex = resultindex + 1 listindex = listindex + 1 return result # }}} def get_indexes(items, reference=REF["IDENTIFIER_FIRST"], index_current=0): # {{{ """ Returns all indexes of the list, which are specified by 'reference' and by the current index 'index_current' (-1 for no current item) in the correct order. I. e.: REF["IDENTIFIER_FIRST"] : all indexes from first to the last item REF["IDENTIFIER_LAST"] : all indexes from the last to first item REF["IDENTIFIER_NEXT"] : all indexes from the next to the last item REF["IDENTIFIER_PREVIOUS"] : all indexes from the previous to the first item """ if (reference in [REF["IDENTIFIER_FIRST"], REF["IDENTIFIER_LAST"]] or index_current == -1): # Read first occurence OR # Read last occurrence OR # No current record (and next/previous occurrence) indexes = range(0, len(items)) if (reference == REF["IDENTIFIER_LAST"] or reference == REF["IDENTIFIER_PREVIOUS"]): # Read last occurrence OR # No current record and previous occurrence indexes.reverse(); elif reference == REF["IDENTIFIER_PREVIOUS"]: # Read previous occurrence indexes = range(0, index_current) indexes.reverse() else: # Read next occurrence indexes = range(index_current + 1, len(items)) return indexes # }}} def prettyprint_anything(indent, thing): # {{{ """ Returns a recursively generated string representation of an object and its attributes. """ s = "%s{%s at 0x%x}:" % (indent, thing.__class__.__name__, id(thing)) indent = indent + " " for (attribute, newvalue) in thing.__dict__.items(): if isinstance(newvalue, int): s = s + "\n" + indent + attribute + (16-len(attribute))*" " + "0x%x" % (newvalue) elif isinstance(newvalue, str): s = s + "\n" + indent + attribute + (16-len(attribute))*" " + "length %d" % len(newvalue) elif isinstance(newvalue, list): s = s + "\n" + indent + attribute + (16-len(attribute))*" " for item in newvalue: s = s + "\n" + prettyprint_anything(indent + " ", item) return s # }}} def make_property(prop, doc): # {{{ """ Assigns a property to the calling object. This is used to decorate instance variables with docstrings. """ return property( lambda self: getattr(self, "_"+prop), lambda self, value: setattr(self, "_"+prop, value), lambda self: delattr(self, "_"+prop), doc) # }}} class File(object): # {{{ """Template class for a smartcard file.""" bertlv_data = make_property("bertlv_data", "list of (tag, length, value)-tuples of BER-TLV coded data objects (encrypted)") lifecycle = make_property("lifecycle", "life cycle byte") parent = make_property("parent ", "parent DF") fid = make_property("fid", "file identifier") filedescriptor = make_property("filedescriptor", "file descriptor byte") simpletlv_data = make_property("simpletlv_data", "list of (tag, length, value)-tuples of SIMPLE-TLV coded data objects (encrypted)") def __init__(self, parent, fid, filedescriptor, lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, SAM=None): """ The constructor is supposed to be involved by creation of a DF or EF. """ if (fid>0xFFFF or fid<0 or fid in [FID["EFDIR"], FID["EFATR"], FID["PATHSELECTION"], FID["RESERVED"]] or filedescriptor>0xFF or lifecycle>0xFF): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.lifecycle=lifecycle self.parent = parent if parent: if parent.fid == fid: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.fid=fid self.filedescriptor=filedescriptor self.SAM = SAM if simpletlv_data: if not isinstance(simpletlv_data, list): raise ValueError, "must be a list of (tag, length, value)-tuples" self.setdec('simpletlv_data', simpletlv_data) if bertlv_data: if not isinstance(bertlv_data, list): raise ValueError, "must be a list of (tag, length, value)-tuples" self.setdec('bertlv_data', bertlv_data) def decrypt(self,path, data): if self.SAM == None: #WARNING: Fails silent return data else: return self.SAM.FSencrypt(path,data) def encrypt(self,path, data): if self.SAM == None: #WARNING: Fails silent return data else: return self.SAM.FSdecrypt(path,data) def __str__(self): """Returns a string of the object using an prettyprint_anything.""" return prettyprint_anything("", self) __repr__ = __str__ def getpath(self): """Returns the path to this file beginning with the MF's fid.""" if self.parent == None: return inttostring(self.fid, 2) else: return self.parent.getpath() + inttostring(self.fid, 2) def getenc(self, attribute): """Returns the decrypted data of an 'attribute' from the file.""" return loads(self.decrypt(self.getpath(), getattr(self, attribute))) def setdec(self, attribute, data): """Encrypts 'data' and sets the file's 'attribute' to the chiffre.""" setattr(self, attribute, dumps(self.encrypt(self.getpath(), data))) def getdata(self, isSimpleTlv, requestedTL): """ Returns a string of either the file's BER-TLV or the file's SIMPLE-TLV coded data objects depending on the bool 'isSimpleTlv'. 'requestedTL' is a list of (tag, length)-tuples that specify which tags should be returned in what size. """ if isSimpleTlv: attribute = 'simpletlv_data' else: attribute = 'bertlv_data' if not hasattr(self, attribute): raise SwError(SW["ERR_NOTSUPPORTED"]) tlv_data = self.getenc(attribute) if requestedTL == []: result = tlv_data else: result = [] for T, L in requestedTL: tagfound = False for i in range(0, len(tlv_data)): tag, _, value = tlv_data[i] if T == tag: tagfound = True if L == 0: result.append(tlv_data[i]) else: result.append((T, L, value[:L])) if not tagfound: raise SwError(SW["ERR_DATANOTFOUND"]) if isSimpleTlv: return simpletlv_pack(result) return bertlv_pack(result) def putdata(self, isSimpleTlv, newtlvlist): """ Sets either the file's BER-TLV or the file's SIMPLE-TLV coded data objects depending on the bool 'isSimpleTlv'. 'newtlvlist' is a list of (tag, length, value)-tuples of new data. """ if isSimpleTlv: attribute = 'simpletlv_data' else: attribute = 'bertlv_data' if not hasattr(self, attribute): raise SwError(SW["ERR_NOTSUPPORTED"]) tlv_data = self.getenc(attribute) for tag, newlength, newvalue in newtlvlist: tagfound = False for i in range(0, len(tlv_data)): t, oldlength, oldvalue = tlv_data[i] if t == tag: # TODO: what if multiple tags can be found? value = write(oldvalue, [newvalue], [0], newlength, self.datacoding) tlv_data[i] = (tag, len(value), value) tagfound = True if not tagfound: tlv_data.append(tag, newlength, newvalue) self.setdec(attribute, tlv_data) def readbinary(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def writebinary(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def updatebinary(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def erasebinary(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def readrecord(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def writerecord(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def appendrecord(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def updaterecord(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) def select(*argz, **args): """Only a template, will raise an error.""" raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) # }}} class DF(File): # {{{ """Class for a dedicated file""" data = make_property("data", "unknown") content = make_property("content", "list of files of the DF") dfname = make_property("dfname", "string with up to 16 bytes. DF name, which can also be used as application identifier.") def __init__(self, parent, fid, filedescriptor=FDB["NOTSHAREABLEFILE"]|FDB["DF"], lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, dfname=None, data=""): """ See File for more. """ File.__init__(self, parent, fid, filedescriptor, lifecycle, simpletlv_data, bertlv_data) if dfname: if len(dfname)>16: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.dfname=dfname self.content = [] # TODO: opensc sends the length of data, but what does it limit # (bertlv-data/simpletlv-data/number of files in DF, ...)? self.setdec('data', data) def __len__(self): """ x.__len__() <==> len(x.content) """ return len(self.content) def __getitem__(self, key): """ x.__getitem__(key) <==> x.content[key] """ return self.content[key] def __setitem__(self, key, value): """ x.__setitem__(key, value) <==> x.content[key]=value """ return self.content[key] def __delitem__(self, key): """ x.__delitem__(key) <==> del x.content[key] """ del content[key] def __contains__(self, item): """ x.__contains__(item) <==> item in x.content """ return item in content def append(self, file): """Appends 'file' to the content of the DF.""" if not (isinstance(file, DF) or isinstance(file, EF)): raise TypeError if (self.fid == file.fid or file.fid == FID["MF"] or file.fid == FID["RESERVED"] or file.fid == FID["PATHSELECTION"]): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) for f in self.content: if f.fid == file.fid: raise SwError(SW["ERR_FILEEXISTS"]) if hasattr(f, 'dfname') and hasattr(file, 'dfname') and f.dfname == file.dfname: raise SwError(SW["ERR_DFNAMEEXISTS"]) if hasattr(f, 'shortfid') and hasattr(file, 'shortfid') and f.shortfid == file.shortfid: raise SwError(SW["ERR_FILEEXISTS"]) self.content.append(file) def select(self, attribute, value, reference=REF["IDENTIFIER_FIRST"], index_current=0): """ Returns the first file of the DF, that has the 'attribute' with the specified 'value'. For partial DF name selection you must specify the first/last/next or previous occurence with 'reference' and the index of the current file 'index_current' (-1 for None). """ indexes = get_indexes(self.content, reference, index_current) for i in indexes: file = self.content[i] if ((hasattr(file, attribute) and getattr(file, attribute)==value) or (attribute == 'dfname' and getattr(file, attribute).startswith(value))): return file # not found if isinstance(value, int): print "file not found %s=%x" % (attribute, value) elif isinstance(value, str): print "file not found %s=%r" % (attribute, value) raise SwError(SW["ERR_FILENOTFOUND"]) def remove(self, file): """Removes 'file' from the content of the DF""" content.remove(file) # }}} class MF(DF): # {{{ """Class for a master file""" current = make_property("current", "the currently selected file") firstSFT = make_property("firstSFT", "string of length 1. The first software function table from the historical bytes.") secondSFT = make_property("secondSFT", "string of length 1. The second software function table from the historical bytes.") def __init__(self, filedescriptor=FDB["NOTSHAREABLEFILE"]|FDB["DF"], lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, dfname=None): """The file identifier FID["MF"] is automatically added. See DF for more. """ DF.__init__(self, None, FID["MF"], filedescriptor, lifecycle, simpletlv_data, bertlv_data, dfname) self.current = self self.firstSFT = inttostring(MF.makeFirstSoftwareFunctionTable(), 1) self.secondSFT = inttostring(MF.makeSecondSoftwareFunctionTable(), 1) @staticmethod def makeFirstSoftwareFunctionTable(# {{{ DFSelectionByFullDFName=True, DFSelectionByPartialDFName=True, DFSelectionByPath=True, DFSelectionByFID=True, DFSelectionByApplication_implicite=True, ShortFIDSupported=True, RecordNumberSupported=True, RecordIdentifierSupported=True): """ Returns a byte according to the first software function table from the historical bytes of the card capabilities. DFSelectionByFullDFName -- Bool (True). DFSelectionByPartialDFName -- Bool (True). DFSelectionByPath -- Bool (True). DFSelectionByFID -- Bool (True). DFSelectionByApplication_implicite -- Bool (True). ShortFIDSupported -- Bool (True). RecordNumberSupported -- Bool (True). RecordIdentifierSupported -- Bool (True). """ fsft = 0 if DFSelectionByFullDFName: fsft |= 1 << 7 if DFSelectionByPartialDFName: fsft |= 1 << 6 if DFSelectionByPath: fsft |= 1 << 5 if DFSelectionByFID: fsft |= 1 << 4 if DFSelectionByApplication_implicite: fsft |= 1 << 3 if ShortFIDSupported: fsft |= 1 << 2 if RecordNumberSupported: fsft |= 1 << 1 if RecordIdentifierSupported: fsft |= 1 return fsft # }}} @staticmethod def makeSecondSoftwareFunctionTable(DCB=DCB["ONETIMEWRITE"]|1): # {{{ """ The second software function table from the historical bytes contains the data coding byte. """ return DCB # }}} def currentDF(self): """Returns the current DF.""" if isinstance(self.current, EF): return self.current.parent else: return self.current def currentEF(self): """Returns the current EF or None if not available.""" if isinstance(self.current, EF): return self.current else: return None @staticmethod def encodeFileControlParameter(file): """ Returns a string of TLV-coded file control information of 'file'. Note: The result is not prepended with tag and length for neither TCP, FMD nor FCI template. """ fdm = [ chr(TAG["FILEIDENTIFIER"])+"\x02"+inttostring(file.fid, 2), chr(TAG["LIFECYCLESTATUS"])+"\x01"+chr(file.lifecycle) ] # TODO filesize and data objects if isinstance(file, EF): if hasattr(file, 'shortfid'): fdm.append("%c\x01%c" % (TAG["SHORTFID"], file.shortfid)) else: fdm.append("%c\x00" % TAG["SHORTFID"]) if isinstance(file, TransparentStructureEF): l = inttostring(len(file.getenc('data'))) fdm.append("%c%c%s" % (TAG["BYTES_EXCLUDINGSTRUCTURE"], chr(len(l)), l)) fdm.append("%c%c%s" % (TAG["BYTES_INCLUDINGSTRUCTURE"], chr(len(l)), l)) fdm.append("%c\x02%c%c" % (TAG["FILEDISCRIPTORBYTE"], file.filedescriptor, file.datacoding)) elif isinstance(file, RecordStructureEF): l = 0 records = file.getenc('records') for r in records: if file.hasSimpleTlv(): l += simpletlv_unpack(r.data)[0][1] else: l += len(r.data) fdm.append("%c\x02%s" % (TAG["BYTES_EXCLUDINGSTRUCTURE"], inttostring(l, 2))) fdm.append("%c\x02%s" % (TAG["BYTES_INCLUDINGSTRUCTURE"], inttostring(l, 2))) l = len(records) fdm.append("%c\x06%c%c%c%c%s" % (TAG["FILEDISCRIPTORBYTE"], file.filedescriptor, file.datacoding, file.maxrecordsize >> 8, file.maxrecordsize & 0x00ff, inttostring(l, 2))) elif isinstance(file, DF): # TODO number of files == number of data bytes? fdm.append("%c\x01%c" % (TAG["FILEDISCRIPTORBYTE"], file.filedescriptor)) if hasattr(file, 'dfname'): fdm.append("%c%c%s" % (TAG["DFNAME"], len(file.dfname), file.dfname)) else: raise TypeError return "".join(fdm) def _selectFile(self, p1, p2, data): """ Returns the file specified by 'p1' and 'data' from the select file command APDU. """ P1_FILE = 0x00 P1_CHILD_DF = 0x01 P1_CHILD_EF = 0x02 P1_PARENT_DF = 0x03 P1_DF_NAME = 0x04 P1_PATH_FROM_MF = 0x08 P1_PATH_FROM_CURRENTDF = 0x09 if (p1>>4) != 0 or p1 == P1_FILE: # RFU OR # When P1='00', the card knows either because of a specific coding # of the file identifier or because of the context of execution of # the command if the file to select is the MF, a DF or an EF. if data[:2] == inttostring(self.fid): selected = walk(self, data[2:]) elif data[:2] == inttostring(self.currentDF().fid): selected = walk(self.currentDF(), data[2:]) else: selected = walk(self.currentDF(), data) elif p1 == P1_CHILD_DF or p1 == P1_CHILD_EF: selected = self.select('fid', stringtoint(data)) if ((p1 == P1_CHILD_DF and not isinstance(selected, DF)) or (p1 == P1_CHILD_EF and not isinstance(selected, EF))): # Command incompatible with file structure raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) elif p1 == P1_PATH_FROM_MF: selected = walk(self, data) elif p1 == P1_PATH_FROM_CURRENTDF: selected = walk(self.currentDF(), data) elif p1 == P1_PARENT_DF: selected = self.current.parent elif p1 == P1_DF_NAME: df = self.currentDF() if df == self or df not in self.content: index_current = -1 else: index_current = self.content.index(df) selected = self.select('dfname', data, p2 & REF["REFERENCE_CONTROL"], index_current) else: print "unknown selection method: p1 =", p1 selected = None if selected == None: raise SwError(SW["ERR_FILENOTFOUND"]) return selected def selectFile(self, p1, p2, data): """ Function for instruction 0xa4. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ P2_FCI = 0 P2_FCP = 1<<2 P2_FMD = 2<<2 P2_NONE = 3<<2 file = self._selectFile(p1, p2, data) if p2 == P2_NONE: data = "" elif p2 == P2_FMD: # TODO data = "" else: if p2 == P2_FCP: tag = TAG["FILECONTROLPARAMETERS"] else: tag = TAG["FILECONTROLINFORMATION"] fdm = MF.encodeFileControlParameter(file) data = bertlv_pack([(tag, len(fdm), fdm)]) self.current = file return SW["NORMAL"], data def dataUnitsDecodePlain(self, p1, p2, data): """ Decodes 'p1', 'p2' and 'data' from a data unit command (i. e. read/write/update/search/erase binary) with *odd* instruction code. Returns the specified TransparentStructureEF, a list of offsets and a list of data strings. """ if p1 >> 7: # If bit 1 of INS is set to 0 and bit 8 of P1 to 1, then bits 7 # and 6 of P1 are set to 00 (RFU), bits 5 to 1 of P1 encode a # short EF identifier and P2 (eight bits) encodes an offset # from zero to 255. ef = self.currentDF().select('shortfid', p1 & 0x1f) self.current = ef offsets = [p2] else: # If bit 1 of INS is set to 0 and bit 8 of P1 to 0, then P1-P2 # (fifteen bits) encodes an offset from zero to 32 767. ef = self.currentEF() if not ef: raise SwError(SW["ERR_NOCURRENTEF"]) offsets = [(p1 << 8) + p2] return ef, offsets, [data] def dataUnitsDecodeEncapsulated(self, p1, p2, data): """ Decodes 'p1', 'p2' and 'data' from a data unit command (i. e. read/write/update/search/erase binary) with *even* instruction code. Returns the specified TransparentStructureEF, a list of offsets and a list of data strings. """ # If bit 1 of INS is set to 1, then P1-P2 shall identify an EF. If # the first eleven bits of P1-P2 are set to 0 and if bits 5 to 1 of # P2 are not all equal and if the card and / or the EF supports # selection by short EF identifier, then bits 5 to 1 of P2 encode a # short EF identifier (a number from one to thirty). Otherwise, # P1-P2 is a file identifier. P1-P2 set to '0000' identifies the # current EF. At least one offset data object with tag '54' shall # be present in the command data field. When present in a command # or response data field, data shall be encapsulated in a # discretionary data object with tag '53' or '73'. tlv_data = bertlv_unpack(data) offsets = decodeOffsetDataObjects(tlv_data) datalist = decodeDiscretionaryDataObjects(tlv_data) if p1 == 0 and p2 >> 5 == 0: ef = self.currentDF().select('shortfid', p2) else: ef = self.currentDF().select('fid', p1 << 8 + p1) self.current = ef return ef, offsets, datalist def readBinaryPlain(self, p1, p2, data): """ Function for instruction 0xb0. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodePlain(p1, p2, data) result = ef.readbinary(offsets[0]) return SW["NORMAL"], result def readBinaryEncapsulated(self, p1, p2, data): """ Function for instruction 0xb1. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodeEncapsulated(p1, p2, data) result = ef.readbinary(offsets[0]) r = encodeDiscretionaryDataObjects([result]) return SW["NORMAL"], r def writeBinaryPlain(self, p1, p2, data): """ Function for instruction 0xd0. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodePlain(p1, p2, data) ef.writebinary(offsets, datalist) return SW["NORMAL"], "" def writeBinaryEncapsulated(self, p1, p2, data): """ Function for instruction 0xd1. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodeEncapsulated(p1, p2, data) ef.writebinary(offsets, datalist) return SW["NORMAL"], "" def updateBinaryPlain(self, p1, p2, data): """ Function for instruction 0xd6. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodePlain(p1, p2, data) ef.updatebinary(offsets, datalist) return SW["NORMAL"], "" def updateBinaryEncapsulated(self, p1, p2, data): """ Function for instruction 0xd7. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodeEncapsulated(p1, p2, data) ef.updatebinary(offsets, datalist) return SW["NORMAL"], "" def searchBinaryPlain(self, p1, p2, data): ef, offsets, datalist = self.dataUnitsDecodePlain(p1, p2, data) r = self.data.find(datalist[0], offsets[0]) if r == -1: raise SwError(SW["ERR_DATANOTFOUND"]) return SW["NORMAL"], inttostring(r) def searchBinaryEncapsulated(self, p1, p2, data): ef, offsets, datalist = self.dataUnitsDecodeEncapsulated(p1, p2, data) if len(offsets) != len(datalist): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) result = [] for data, offset in offsets, datalist: r = self.data.find(data, offset) if r == -1: raise SwError(SW["ERR_DATANOTFOUND"]) result.append(inttostring(r)) return SW["NORMAL"], encodeDataOffsetObjects(result) def eraseBinaryPlain(self, p1, p2, data): """ Function for instruction 0x0e. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodePlain(p1, p2, data) # If INS = '0E', then, if present, the command data field encodes # the offset of the first data unit not to be erased. This offset # shall be higher than the one encoded in P1-P2. If the data field # is absent, then the command erases up to the end of the file. erasefrom = offsets[0] if data: tlv_data = bertlv_unpack(data) eraseto = decodeOffsetDataObjects(tlv_data)[0] else: eraseto = None ef.erasebinary(erasefrom, eraseto) return SW["NORMAL"], "" def eraseBinaryEncapsulated(self, p1, p2, data): """ Function for instruction 0x0f. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, offsets, datalist = self.dataUnitsDecodeEncapsulated(p1, p2, data) # If INS = '0F', then, if present, the command data field shall # consist of zero, one or two offset data objects. If there is no # offset, then the command erases all the data units in the file. # If there is one offset, it indicates the first data unit to be # erased; then the command erases up to the end of the file. Two # offsets define a sequence of data units: the second offset # indicates the first data unit not to be erased; it shall be # higher than the first offset. if len(offsets) > 1: eraseto = offsets[1] else: eraseto = None if len(offsets) > 0: erasefrom = offsets[0] else: erasefrom = None ef.erasebinary(erasefrom, eraseto) return SW["NORMAL"], "" def recordHandlingDecode(self, p1, p2): """ Decodes 'p1' and 'p2' from a record handling command (i. e. read/write/update/append/search/erase record). Returns the specified RecordStructureEF, the number or identifier of the record and a reference, that specifies which record to select (i. e. the last 3 bits of 'p1'). """ if p1 == 0xff: # RFU raise SwError(SW["ERR_INCORRECTP1P2"]) else: num_id = p1 shortfid = p2 >> 3 if shortfid == 0: ef = self.currentEF() if ef == None: raise SwError(SW["ERR_NOCURRENTEF"]) elif shortfid == 0x1f: # RFU raise SwError(SW["ERR_INCORRECTP1P2"]) else: ef = self.currentDF().select('shortfid', shortfid) self.current = ef ef.resetRecordPointer() return ef, num_id, p2 & REF["REFERENCE_CONTROL"] def readRecordPlain(self, p1, p2, data): """ Function for instruction 0xb2. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) result = ef.readrecord(0, num_id, reference) r = "" for item in result: r += item return SW["NORMAL"], r def readRecordEncapsulated(self, p1, p2, data): """ Function for instruction 0xb3. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) result = ef.readrecord(0, num_id, reference) return SW["NORMAL"], encodeDiscretionaryDataObjects(result) def writeRecord(self, p1, p2, data): """ Function for instruction 0xd2. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) if reference not in [ REF["IDENTIFIER_FIRST"], REF["IDENTIFIER_LAST"], REF["IDENTIFIER_NEXT"], REF["IDENTIFIER_PREVIOUS"], REF["NUMBER"]]: # RFU raise SwError(SW["ERR_INCORRECTPARAMETERS"]) ef.writerecord(num_id, reference, 1, data) return SW["NORMAL"], "" def updateRecordPlain(self, p1, p2, data): """ Function for instruction 0xdc. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) if reference not in [ REF["IDENTIFIER_FIRST"], REF["IDENTIFIER_LAST"], REF["IDENTIFIER_NEXT"], REF["IDENTIFIER_PREVIOUS"], REF["NUMBER"]]: # RFU raise SwError(SW["ERR_INCORRECTPARAMETERS"]) ef.updaterecord(num_id, reference, 0, data) return SW["NORMAL"], "" def updateRecordEncapsulated(self, p1, p2, data): """ Function for instruction 0xdd. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) P2_REPLACE = 0x04 P2_AND = 0x05 P2_OR = 0x06 #P2_XOR = 0x07 tlv_data = bertlv_unpack(data) if reference in [ REF["IDENTIFIER_FIRST"], REF["IDENTIFIER_LAST"], REF["IDENTIFIER_NEXT"], REF["IDENTIFIER_PREVIOUS"]]: # RFU raise SwError(SW["ERR_INCORRECTPARAMETERS"]) elif reference == P2_REPLACE: ef.writerecord(num_id, reference, decodeOffsetDataObjects(tlv_data)[0], decodeDiscretionaryDataObjects(tlv_data)[0], DCB["ONETIMEWRITE"]) elif reference == P2_AND: ef.writerecord(num_id, reference, decodeOffsetDataObjects(tlv_data)[0], decodeDiscretionaryDataObjects(tlv_data)[0], DCB["WRITEAND"]) elif reference == P2_OR: ef.writerecord(num_id, reference, decodeOffsetDataObjects(tlv_data)[0], decodeDiscretionaryDataObjects(tlv_data)[0], DCB["WRITEOR"]) else: # reference == P2_XOR: ef.writerecord(num_id, reference, decodeOffsetDataObjects(tlv_data)[0], decodeDiscretionaryDataObjects(tlv_data)[0], DCB["PROPRIETARY"]) return SW["NORMAL"], "" def appendRecord(self, p1, p2, data): """ Function for instruction 0xe2. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ if p1 != 0 or (p2 & REF["REFERENCE_CONTROL"]) != 0: raise SwError(SW["ERR_INCORRECTP1P2"]) ef, num_id, reference = self.recordHandlingDecode(p1, p2) sw = ef.appendrecord(data) return SW["NORMAL"], "" def eraseRecord(self, p1, p2, data): """ Function for instruction 0x0c. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ ef, num_id, reference = self.recordHandlingDecode(p1, p2) if reference not in [ REF["NUMBER"], REF["NUMBER_TO_LAST"]]: # RFU raise SwError(SW["ERR_INCORRECTPARAMETERS"]) ef.eraserecord(num_id, reference) return SW["NORMAL"], "" def dataObjectHandlingDecodePlain(self, p1, p2, data): """ Decodes 'p1', 'p2' and 'data' from a data object handling command (i. e. get/put data) with *even* instruction code. Returns the specified file, True if the following list regards SIMPLE-TLV data objects False otherwise and a list of (tag, length, value)-tuples. """ if self.current == None: raise SwError(SW["ERR_NOCURRENTEF"]) file = self.current if (p1 == 0 and 0x40 <= p2 and p2 <= 0xfe) or (0x40 <= p1 and p2 != 0 and p2 != 0xff): # If bit 1 of INS is set to 0 and P1 to '00', then P2 from '40' to # 'FE' shall be a BER-TLV tag on a single byte. OR # If bit 1 of INS is set to 0 and if P1-P2 lies from '4000' to # 'FFFF', then they shall be a BER-TLV tag on two bytes. tlv_data = [((p1<<8) + p2, len(data), data)] isSimpleTlv = False elif p1 == 0x02 and 0x01 <= p2 and p2 <= 0xfe: # If bit 1 of INS is set to 0 and P1 to '02', then P2 from '01' to # 'FE' shall be a SIMPLE-TLV tag. tlv_data = [(p2, len(data), data)] isSimpleTlv = True elif p1 == 0x00 and p2 == 0xff: # The value '00FF' is used either for obtaining all the common # BER-TLV data objects readable in the context, or for indicating # that the command data field is encoded in BER-TLV. if data: tlv_data = bertlv_unpack(data) else: tlv_data = [] isSimpleTlv = False elif p1 == 0x02 and p2 == 0xff: # The value '02FF' is used either for obtaining all the common # SIMPLE-TLV data objects readable in the context or for indicating # that the command data field is encoded in SIMPLE-TLV. if data: tlv_data = simpletlv_unpack(data) else: tlv_data = [] isSimpleTlv = True else: raise SwError(SW["ERR_NOTSUPPORTED"]) return file, isSimpleTlv, tlv_data def dataObjectHandlingDecodeEncapsulated(self, p1, p2, data): """ Decodes 'p1', 'p2' and 'data' from a data object handling command (i. e. get/put data) with *odd* instruction code. Returns the specified file, True if the following list regards SIMPLE-TLV data objects False otherwise and a list of (tag, length, value)-tuples. """ # If bit 1 of INS is set to 1, then P1-P2 shall identify a file. tlv_data = bertlv_unpack(data) if p1 == 0 and p2 == 0: # P1-P2 set to '0000' identifies the current EF, unless the command # data field provides a file reference data object (tag '51', see # 5.3.1.2) for identifying a file. file = getfile_byrefdataobj(self, tlv_find_tag(tlv_data, TAG["FILE_REFERENCE"])[0]) if file == None: file = self.currentEF() if file == None: raise SwError(SW["ERR_NOCURRENTEF"]) elif p1 == 0 and (p2 >> 5) == 0: # If the first eleven bits of P1-P2 are set to 0 and if bits 5 to 1 # of P2 are not all equal and if the card and / or the file # supports selection by short EF identifier, then bits 5 to 1 of P2 # encode a short EF identifier (a number from one to thirty). file = self.currentDF().select('shortfid', p2) elif p1 == 0x3f and p2 == 0xff: # P1-P2 set to '3FFF' identifies the current DF. file = self.currentDF() else: # Otherwise, P1-P2 is a file identifier. file = self.currentDF().select('fid', p1<<8 + p2) if file == None: raise SwError(SW["ERR_FILENOTFOUND"]) self.current = file return file, tlv_data def getDataPlain(self, p1, p2, data): """ Function for instruction 0xca. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ file, isSimpleTlv, tlvlist = self.dataObjectHandlingDecodePlain(p1, p2, data) # TODO oversized answers if len(tlvlist) > 0: return SW["NORMAL"], file.getdata(isSimpleTlv, [(tlvlist[0][0], 0)]) else: return SW["NORMAL"], file.getdata(isSimpleTlv, []) def getDataEncapsulated(self, p1, p2, data): """ Function for instruction 0xcb. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ file, tlv_data = self.dataObjectHandlingDecodeEncapsulated(p1, p2, data) # TODO oversized answers requestedTL = decodeTagList(tlv_data) if requestedTL == []: requestedTL = decodeHeaderList(tlv_data) if requestedTL == []: requestedTL = decodeExtendedHeaderList(tlv_data) return SW["NORMAL"], file.getdata(False, requestedTL) def putDataPlain(self, p1, p2, data): """ Function for instruction 0xda. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ file, isSimpleTlv, tlvlist = self.dataObjectHandlingDecodePlain(p1, p2, data) file.putdata(isSimpleTlv, tlvlist) return SW["NORMAL"], "" def putDataEncapsulated(self, p1, p2, data): """ Function for instruction 0xdb. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ file, tlvlist = self.dataObjectHandlingDecodeEncapsulated(p1, p2, data) file.putdata(False, tlvlist) return SW["NORMAL"], "" @staticmethod def create(p1, p2, data): """ Creates and returns a list of files according to the parameters of a create file command. """ def fdb2args(value, args): l = len(value) if l >= 5: # TODO number of records on one or two bytes raise SwError(SW["ERR_NOTSUPPORTED"]) if l >= 3: args["maxrecordsize"] = stringtoint(value[2:]) if l >= 2: args["datacoding"] = ord(value[1]) if l >=1: args["filedescriptor"] = ord(value[0]) def shortfid2args(value, args): s = stringtoint(value) if (s & 7) == 0: shortfid = s >> 3 if shortfid != 0: args["shortfid"] = shortfid def unknown(tag, value): print "unknown tag 0x%x with %r" % tag, value tag2cmd = { # TODO support other tags TAG["FILEDISCRIPTORBYTE"] : 'fdb2args(value, args)', TAG["FILEIDENTIFIER"] : 'args["fid"] = stringtoint(value)', TAG["DFNAME"] : 'args["dfname"] = value', TAG["SHORTFID"] : 'shortfid2args(value, args)', TAG["LIFECYCLESTATUS"] : 'args["lifecycle"] = stringtoint(value)', TAG["BYTES_EXCLUDINGSTRUCTURE"] : 'args["data"] = chr(0)*stringtoint(value)', TAG["BYTES_INCLUDINGSTRUCTURE"] : 'args["data"] = chr(0)*stringtoint(value)', } fcp_list = tlv_find_tags( bertlv_unpack(data), [TAG["FILECONTROLINFORMATION"], TAG["FILECONTROLPARAMETERS"]]) if not fcp_list: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) files = [] args = { "parent": None } if p1 != 0: args["filedescriptor"] = p1 if (p2>>3) != 0: args["shortfid"] = p2>>3 for T, _, tlv_data in fcp_list: if T != TAG["FILECONTROLPARAMETERS"] and T != TAG["FILECONTROLINFORMATION"]: raise ValueError for tag, _, value in tlv_data: exec tag2cmd.get(tag, 'unknown(tag, value)') in locals(), globals() if (args["filedescriptor"] & FDB["DF"]) == FDB["DF"]: # FIXME: data for DF if "data" in args: print 'what to do with DF-data %r?' % args["data"] del args["data"] file = DF(**args) elif ((args["filedescriptor"] & 7) in [FDB["EFSTRUCTURE_NOINFORMATIONGIVEN"], FDB["EFSTRUCTURE_TRANSPARENT"]]): file = TransparentStructureEF(**args) file.writebinary( decodeOffsetDataObjects(tlv_data), decodeDiscretionaryDataObjects(tlv_data) ) else: file = RecordStructureEF(**args) files.append(file) return files def createFile(self, p1, p2, data): """ Function for instruction 0xe0. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ df = self.currentDF() if df == None: raise SwError(SW["ERR_NOCURRENTEF"]) for file in self.create(p1, p2, data): file.parent = df df.append(file) self.current = file return SW["NORMAL"], "" def deleteFile(self, p1, p2, data): """ Function for instruction 0xe4. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ file = self._selectFile(p1, p2, data) file.parent.content.remove(file) # FIXME: free memory of file and remove its content from the security device return SW["NORMAL"], "" # }}} class EF(File): # {{{ """Template class for an elementary file.""" shortfid = make_property("shortfid", "integer with 1<=shortfid<=30. Short EF identifier.") datacoding = make_property("datacoding", "integer. Data coding byte.") def __init__(self, parent, fid, filedescriptor, lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, datacoding=DCB["ONETIMEWRITE"], shortfid=0): """ The constructor is supposed to be involved creation of a by creation of a TransparentStructureEF or RecordStructureEF. See File for more. """ # exlcude FIDs for DFs if fid == FID["MF"] or datacoding>0xFF: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) if shortfid: if not (1 <= shortfid and shortfid <= 30): raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.shortfid=shortfid File.__init__(self, parent, fid, filedescriptor, lifecycle, simpletlv_data, bertlv_data) self.datacoding=datacoding # }}} class TransparentStructureEF(EF): # {{{ """Class for an elementary file with transparent structure.""" data = make_property("data", "string (encrypted). The file's data.") def __init__(self, parent, fid, filedescriptor=FDB["EFSTRUCTURE_TRANSPARENT"], lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, datacoding=DCB["ONETIMEWRITE"], shortfid=0, data=""): """ See EF for more. """ EF.__init__(self, parent, fid, filedescriptor, lifecycle, simpletlv_data, bertlv_data, datacoding, shortfid) self.setdec('data', data) def readbinary(self, offset): """Returns the string of decrypted data beginning at 'offset'.""" data = self.getenc('data') if offset > len(data): raise SwError(SW["ERR_OFFSETOUTOFFILE"]) return data[offset:] def writebinary(self, offsets, datalist, datacoding=None): """ Writes pieces of data to the specified offsets honoring the given coding byte. offsets -- list of integers. Offsets. datalist -- list of strings. Data pieces. datacoding -- the data coding byte to use for writing """ data = self.getenc('data') if datacoding: data = write(data, datalist, offsets, datacoding) else: data = write(data, datalist, offsets, self.datacoding) self.setdec('data', data) def updatebinary(self, offsets, datalist): """ x.updatebinary(offsets, datalist) <==> x.writebinary(offsets, datalist, DCB["ONETIMEWRITE"]) """ return self.writebinary(offsets, datalist, DCB["ONETIMEWRITE"]) def erasebinary(self, erasefrom, eraseto): """ Sets (part of) the content of an EF to its logical erased state, sequentially starting from 'erasefrom' ending at 'eraseto'. """ data = self.getenc('data') if erasefrom == None: erasefrom = 0 if eraseto == None: eraseto = len(data) if erasefrom>len(data): raise SwError(SW["ERR_OFFSETOUTOFFILE"]) if erasefrom>eraseto: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) data=data[0:erasefrom] + data[eraseto:len(data)] self.setdec('data', data) # }}} class Record(object): # {{{ data = make_property("data", "string. The record's data.") identifier = make_property("identifier", "integer with 1< = identifier< = 0xfe. The record's identifier.") """Class for a Record of an elementary of record structure""" def __init__(self, identifier=None, data=""): """ The constructor is supposed to be involved by EF.appendrecord. """ if identifier: if identifier<0x01 or identifier>0xFE: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) self.identifier=identifier self.data=data def __str__(self): """Returns a string of the object using an prettyprint_anything.""" return prettyprint_anything("", self) __repr__ = __str__ # }}} class RecordStructureEF(EF): # {{{ """Class for an elementary file with record structure.""" records = make_property("records", "list of records (encrypted)") maxrecordsize = make_property("maxrecordsize", "integer. maximum length of a record's data.") recordpointer = make_property("recordpointer", "integer. Points to the current record (i. e. index of records).") def __init__(self, parent, fid, filedescriptor, lifecycle=LCB["ACTIVATED"], simpletlv_data=None, bertlv_data=None, datacoding=DCB["ONETIMEWRITE"], shortfid=0, maxrecordsize=0xffff, records=[]): """ You should specify the appropriate file descriptor byte to specify which kind of record structured file you want to create (i. e. linear/cyclic or variable/fixed EF). The record pointer is reset. records -- list of Records maxrecordsize -- integer. maximum length of a record's data. See EF for more. """ if not isinstance(records, list): raise TypeError, "must be a list of Records" EF.__init__(self, parent, fid, filedescriptor, lifecycle, simpletlv_data, bertlv_data, datacoding, shortfid) for r in records: if len(r.data) > maxrecordsize or (self.hasFixedRecordSize() and len(r.data) < maxrecordsize): raise SwError(SW["ERR_INCOMPATIBLEWITHFILE"]) self.setdec('records', records) self.resetRecordPointer() self.maxrecordsize=maxrecordsize def resetRecordPointer(self): """Resets the record pointer.""" self.recordpointer=-1 def isCyclic(self): """Returns True if the EF is of cyclic structure, False otherwise.""" attr = self.filedescriptor & 0x07 if (attr==FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"] or attr==FDB["EFSTRUCTURE_CYCLIC_SIMPLETLV"]): return True else: return False def hasSimpleTlv(self): """Returns True if the EF is of TLV structure, False otherwise.""" attr = self.filedescriptor & 0x03 if (attr==FDB["EFSTRUCTURE_LINEAR_FIXED_SIMPLETLV"] or attr==FDB["EFSTRUCTURE_LINEAR_VARIABLESIMPLETLV"] or attr==FDB["EFSTRUCTURE_LINEAR_VARIABLESIMPLETLV"]): return True else: return False def hasFixedRecordSize(self): """Returns True if the records are of fixed size, False otherwise.""" if (self.filedescriptor == FDB["EFSTRUCTURE_LINEAR_FIXED_SIMPLETLV"] or self.filedescriptor == FDB["EFSTRUCTURE_LINEAR_FIXED_NOFURTHERINFO"]): return True else: return False def __getRecords(self, num_id, reference): """ Returns a list of records. num_id -- The requested record's number or identifier reference -- Specifies which record to select (usually the last 3 bits of 'p1' of a record handling command) """ if (reference >> 2) == 1: return self.__getRecordsByNumber(num_id, reference) else: return self.__getRecordsByIdentifier(num_id, reference) def __getRecordsByNumber(self, number, reference): """ Returns a list of records. Is to be involved by __getRecords. number -- The requested record's number reference -- Specifies which record to select (usually the last 3 bits of 'p1' of a record handling command) """ result = [] records = self.getenc('records') if number == 0: # refer to the current record start = self.recordpointer else: start = number - 1 if reference == REF["NUMBER"]: end = start + 1 else: # Read all records from number up to the last OR # Read all records from the last up to number end = len(records) result = records[start:end] if reference == REF["NUMBER_FROM_LAST"]: result.reverse() if result == []: raise SwError(SW["ERR_RECORDNOTFOUND"]) return result def __getRecordsByIdentifier(self, id, reference): """ Returns a list of records and sets the recordpointer to the first record, that matched. Is to be involved by __getRecords. id -- The requested record's identifier reference -- Specifies which record to select (usually the last 3 bits of 'p1' of a record handling command) """ result = [] records = self.getenc('records') indexes = get_indexes(records, reference, self.recordpointer) for i in indexes: if (not self.hasSimpleTlv()) or records[i].identifier==id: if result == []: self.recordpointer=i result.append(records[i]) if result == []: self.resetRecordPointer() raise SwError(SW["ERR_RECORDNOTFOUND"]) return result def readrecord(self, offset, num_id, reference): """ Returns a data string from the given 'offset'. 'num_id' and 'reference' specify the record (see __getRecords). """ records = self.__getRecords(num_id, reference) result = [] for r in records: result.append(r.data[offset:]) if offset == 0: pass elif offset > 0 and offset > len(r.data): offset -= len(r.data) else: offset = 0 return result def writerecord(self, num_id, reference, offset, data, datacoding=None): """ Writes a data string to the 'offset' of a record using the given data coding byte. 'num_id' and 'reference' specify the record (see __getRecords). """ if self.isCyclic() and reference == REF["IDENTIFIER_PREVIOUS"]: return self.appendrecord(data) records = self.__getRecords(num_id, reference) if datacoding: records[0].data = write(records[0].data, [data], [0], datacoding, self.maxrecordsize) else: records[0].data = write(records[0].data, [data], [0], self.datacoding, self.maxrecordsize) if self.hasSimpleTlv(): # identifier/tag could have changed records[0].identifier = simpletlv_unpack(records[0].data)[0][0] def updaterecord(self, num_id, reference, offset, data): """ x.updaterecord(num_id, reference, offset, data) <==> x.writerecord(num_id, reference, offset, data, DCB["ONETIMEWRITE"]) """ return writerecord(num_id, reference, offset, data, DCB["ONETIMEWRITE"]) def appendrecord(self, data): """ Appends a new record to the file, initializing it with the given data string. Sets the recordpointer to the newly created record. """ if self.hasSimpleTlv(): recordidentifier = simpletlv_unpack(data)[0][0] else: recordidentifier = None l = len(data) if l > self.maxrecordsize: raise SwError(SW["ERR_INCORRECTPARAMETERS"]) if self.hasFixedRecordSize(): data = chr(0)*(self.maxrecordsize) + data records = self.getenc('records') if self.isCyclic(): records.insert(0, Record(recordidentifier, data)) self.recordpointer=0 else: records.append(Record(recordidentifier, data)) self.recordpointer=len(records)-1 self.setdec('records', records) def eraserecord(self, num_id, reference): """ Removes a record from the file. 'num_id' and 'reference' specify the record (see __getRecords). """ records = self.__getRecords(num_id, reference) for r in records: r.data = "" r.identifier = None return SW["NORMAL"] # }}} class CryptoflexMF(MF): # {{{ @staticmethod def create(p1, p2, data): if data[0:2] != "\xff\xff": raise SwError(SW["ERR_INCORRECTPARAMETERS"]) args = { "parent": None, "filedescriptor": 0, "fid": stringtoint(data[4:6]), } if data[6] == "\x01": args["data"] = chr(0)*stringtoint(data[2:4]) args["filedescriptor"] = FDB["EFSTRUCTURE_TRANSPARENT"] file = TransparentStructureEF(**args) elif data[6] == "\x02": if len(data)>16: args["maxrecordsize"] = stringtoint(data[16]) elif p2: # if given a number of records args["maxrecordsize"] = (stringtoint(data[2:4])/p2) args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_FIXED_NOFURTHERINFO"] file = RecordStructureEF(**args) elif data[6] == "\x03": args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_VARIABLE_NOFURTHERINFO"] file = RecordStructureEF(**args) elif data[6] == "\x04": args["filedescriptor"] = FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"] file = RecordStructureEF(**args) elif data[6] == "\x38": if data[12] != "\x03": raise SwError(SW["ERR_INCORRECTPARAMETERS"]) file = DF(**args) else: print "unknown type: 0x%x" % ord(data[6]) raise SwError(SW["ERR_INCORRECTPARAMETERS"]) return [file] def recordHandlingDecode(self, p1, p2): if p2 < 4: p1 = 0 return MF.recordHandlingDecode(self, p1, p2) def dataUnitsDecodePlain(self, p1, p2, data): ef = self.currentEF() # FIXME: Cyberflex Access TM Software Development Kit Release 3C # enforces the following: # offsets = [(p2 << 8) + p1] # but smartcard_login-0.1.1 requires: offsets = [(p1 << 8) + p2] datalist = [data] return ef, offsets, datalist def selectFile(self, p1, p2, data): """ Function for instruction 0xa4. Takes the parameter bytes 'p1', 'p2' as integers and 'data' as binary string. Returns the status bytes as two byte long integer and the response data as binary string. """ P2_FCI = 0 P2_FCP = 1<<2 P2_FMD = 2<<2 P2_NONE = 3<<2 file = self._selectFile(p1, p2, data) if isinstance(file, EF): size = inttostring(min(0xffff, len(file.getenc('data'))), 2) # File size (body only) extra = chr(0) # RFU if isinstance(file, RecordStructureEF) and file.hasFixedRecordSize() and not file.isCyclic(): extra += chr(0) + chr(min(file.records, 0xff)) # Length of records elif isinstance(file, DF): size = inttostring(0xffff, 2) # Number of unused EEPROM bytes available in the DF efcount = 0 dfcount = 0 chv1 = None chv2 = None chv1lifecycle = 0 chv2lifecycle = 0 for f in self.content: if f.fid == 0x0000: chv1 = f chv1lifecycle = f.lifecycle & 1 if f.fid == 0x0100: chv2 = f chv2lifecycle = f.lifecycle & 1 if isinstance(f, EF): efcount += 1 if isinstance(f, DF): dfcount += 1 if chv1: extra = chr(1) # TODO LSB correct? else: extra = chr(0) # TODO LSB correct? extra += chr(efcount) extra += chr(dfcount) extra += chr(0) # TODO Number of PINs and unblock CHV PINs extra += chr(0) # RFU if chv1: extra += chr(0) # TODO Number of remaining CHV1 attempts extra += chr(0) # TODO Number of remaining unblock CHV1 attempts if chv2: extra += chr(0) # TODO CHV2 key status extra += chr(0) # TODO CHV2 unblocking key status data = inttostring(0, 2) # RFU data += size data += inttostring(file.fid, 2) data += inttostring(file.filedescriptor) # File type data += inttostring(0, 4) # ACs TODO data += chr(file.lifecycle & 1) # File status data += chr(len(extra)) data += extra self.current = file return SW["NORMAL"], data # }}}