From 0aa419f17a7a2c3708ccc3357f5cfc06a70a0222 Mon Sep 17 00:00:00 2001 From: ronys Date: Fri, 22 Jul 2011 13:07:04 +0300 Subject: [PATCH] First commit of Paul McIntyre's Python library for pwsafe file processing --- LICENSE | 18 + PWSafeV3Headers.py | 431 +++++++++++++++ PWSafeV3Records.py | 1275 ++++++++++++++++++++++++++++++++++++++++++++ README | 23 + __init__.py | 431 +++++++++++++++ consts.py | 759 ++++++++++++++++++++++++++ errors.py | 72 +++ 7 files changed, 3009 insertions(+) create mode 100755 LICENSE create mode 100755 PWSafeV3Headers.py create mode 100755 PWSafeV3Records.py create mode 100644 README create mode 100755 __init__.py create mode 100755 consts.py create mode 100755 errors.py diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..c99bd51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== \ No newline at end of file diff --git a/PWSafeV3Headers.py b/PWSafeV3Headers.py new file mode 100755 index 0000000..709f5a6 --- /dev/null +++ b/PWSafeV3Headers.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== +"""Header objects for psafe v03 + +# Other header types we don't handle yet +, Header(2,23,'\x17\x00\x00\x00\x02B 28 1 B 29 1 I 12 255 k\xd6\xa9*') +, Header(3,4,'\x04\x00\x00\x00\x031111J\xa9\x8a\xbd\x15Ou') +, Header(4,4,'\x04\x00\x00\x00\x04\xf1\x9fpId\xfbeb\xff[\x9a') +, Header(7,16,'\x10\x00\x00\x00\x07paulson_mcintyre\xa7\xcd\x90\x03nx0\xf7{\xd4\x8a') +, Header(8,9,'\t\x00\x00\x00\x08L-LV01203\xd1\x12') +, Header(6,19,'\x13\x00\x00\x00\x06Password Safe V3.11\xc3\n\x85\x93\x95\x82z\t') + +""" +# Note: Use "=" in all packs to account for 64bit systems + +from struct import unpack, pack +from errors import * +from consts import * +import os +import logging, logging.config +from uuid import UUID, uuid4 +from pprint import pformat + +#logging.config.fileConfig('/etc/mss/psafe_log.conf') +log = logging.getLogger("psafe.lib.header") +log.debug('initing') + +class Header(object): + """A psafe3 header object. Should be extended. This also servers as a "unknown" header type. + raw_data string Real data that was passed + data string Raw data minus padding and headers + len long Number of bytes of data. May not be present until data has been parsed + readblock_f function Read in another block of data + TYPE int Header type that IDs it in psafe3 + + """ + TYPE = None + def __init__(self, htype, hlen, raw_data): + self.data = raw_data[5:(hlen + 5)] + self.raw_data = raw_data + self.len = int(hlen) + if type(self) != Header: + assert self.TYPE == htype + else: + self.TYPE = htype + self.parse() + + def parse(self): + """Parse the header. Should be overridden. """ + pass + + def gen_blocks(self): + """Returns the raw data that should be stuck in a psafe file""" + return self.raw_data + + def __repr__(self): + # Can no longer depend on raw_data existing + #return "Header(%s,%d,%s)"%(repr(self.TYPE),self.len,repr(self.raw_data)) + s = self.serial() + return "Header(%s,%d,%s)" % (repr(self.TYPE), len(s), repr(s)) + + def __str__(self): + return self.__repr__() + + def hmac_data(self): + """Returns the data segments that should be used for the HMAC. See bug 1812081. """ + return self.serial() + + def serial(self): + return self.data + + def serialiaze(self): + serial = self.serial() + log.debug("len: %s type: %s final: %s" % (len(serial), repr(chr(self.TYPE)), repr(pack('=lc', len(serial), chr(self.TYPE))))) + padded = self._pad(pack('=lc', len(serial), chr(self.TYPE)) + serial) + log.debug("Padded data %s" % repr(padded)) + return padded + + def _pad(self, data): + """ Pad out data to 16 bytes """ + add_data = 16 - len(data) % 16 + if add_data == 16: + add_data = 0 + padding = '' + for i in xrange(0, add_data): + padding += os.urandom(1) + assert len(padding) == add_data + assert len(data + padding) % 16 == 0 + return data + padding + + +class VersionHeader(Header): + """Version header object + version int Psafe version + +>>> x=VersionHeader(0,2,'\x02\x00\x00\x00\x00\x02\x03\xb45C\x1d\xea\x08\x155\x02') +>>> str(x) +'Version=0x302' +>>> repr(x) +"VersionHeader(0,2,'\\x02\\x00\\x00\\x00\\x00\\x02\\x03\\xb45C\\x1d\\xea\\x08\\x155\\x02')" +>>> x.serial() +'\x02\x03' +>>> x=VersionHeader(version=0x304) +>>> str(x) +'Version=0x304' +>>> x.serial() +'\x04\x03' + """ + TYPE = 0x00 + + def __init__(self, htype=None, hlen=2, raw_data=None, version=0x305): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + self.version = version + + def parse(self): + """Parse data""" + self.version = unpack('=H', self.data)[0] + + def __repr__(self): + return "Version" + Header.__repr__(self) + + def __str__(self): + return "Version=%s" % hex(self.version) + + def serial(self): + return pack('=H', self.version) + +from uuid import UUID +class UUIDHeader(Header): + """DB UUID + uuid uuid.UUID Database uuid object + +DHeader(1,16,'\x10\x00\x00\x00\x01\xbdV\x92{H\xdbL\xec\xbb+\xe90w5\x17\xa2P6b\xe8\x87\x0c\x83\n\xd8\x11\xd7') +>>> x.serial() +'\xbdV\x92{H\xdbL\xec\xbb+\xe90w5\x17\xa2' +>>> str(x) +"UUID=UUID('bd56927b-48db-4cec-bb2b-e930773517a2')" +>>> repr(x) +"UUIDHeader(1,16,'\\x10\\x00\\x00\\x00\\x01\\xbdV\\x92{H\\xdbL\\xec\\xbb+\\xe90w5\\x17\\xa2P6b\\xe8\\x87\\x0c\\x83\\n\\xd8\\x11\\xd7')" + """ + TYPE = 0x01 + + def __init__(self, htype=None, hlen=16, raw_data=None): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + self.uuid = uuid4() + + def parse(self): + """Parse data""" + self.uuid = UUID(bytes=unpack('=16s', self.data)[0]) + + def __repr__(self): + return "UUID" + Header.__repr__(self) + + def __str__(self): + return "UUID=%s" % repr(self.uuid) + + def serial(self): + return pack('=16s', str(self.uuid.bytes)) + +class NonDefaultPrefsHeader(Header): + """Version header object + version int Psafe version + opts dict All config options + +K:V for opts: + + +>>> x=NonDefaultPrefsHeader(2,70,'B 1 1 B 2 1 B 28 1 B 29 1 B 31 1 B 50 0 I 12 255 I 17 1 I 18 1 I 20 1 ') +>>> x=NonDefaultPrefsHeader(2,86,'B 1 1 B 2 1 B 28 1 B 29 1 B 31 1 B 50 0 I 12 255 I 17 1 I 18 1 I 20 1 S 3 \'adfasdfs"\' ') +# FIXME: Fill in tests + """ + TYPE = 0x02 + + def __init__(self, htype=None, hlen=2, raw_data=None, **kw): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + self.opts = kw + + def parse(self): + """Parse data""" + self.opts = {} + remander = self.data.split(' ') + while len(remander) > 2: + # Pull out the data + rtype = str(remander[0]) + key = int(remander[1]) + value = str(remander[2]) + del remander[0:3] + if rtype == "B": + found = False + for name, info in conf_bools.items(): + if info['index'] == key: + found = True + break + if not found: + raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key + if value == "0": + self.opts[name] = False + elif value == "1": + self.opts[name] = True + else: + raise PrefsValueError, "Expected either 0 or 1 for bool type, got %r" % value + elif rtype == "I": + found = False + for name, info in conf_ints.items(): + if info['index'] == key: + found = True + break + if not found: + raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key + try: + value = int(value) + except ValueError: + raise PrefsDataTypeError, "%r is not a valid int" % value + if info['min'] != -1 and info['min'] > value: + raise PrefsDataTypeError, "%r is too small" % value + if info['max'] != -1 and info['max'] < value: + raise PrefsDataTypeError, "%r is too big" % value + self.opts[name] = value + elif rtype == "S": + found = False + for name, info in conf_strs.items(): + if info['index'] == key: + found = True + break + if not found: + raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key + # Remove "" or whatever the delimiter is + delm = value[0] + if value[-1] == delm: + value = value[1:-1] + else: + while not delm in remander[0] and len(remander) > 0: + value += remander[0] + del remander[0] + value = value[1:-1] + # Save the pref + self.opts[name] = value + else: + raise PrefsDataTypeError, "Unexpected record type for preferences %r" % rtype + + def __repr__(self): + return "NonDefaultPrefs" + Header.__repr__(self) + + def __str__(self): + return "NonDefaultPrefs=%s" % pformat(self.opts) + + def serial(self): + ret = '' + for name, value in self.opts.items(): + if not conf_types.has_key(name): + raise PrefsValueError, "%r is not a valid configuration option" % name + typ = conf_types[name] + if type(value) != typ: + raise PrefsDataTypeError, "%r is not a valid type for the key %r" % (type(value), name) + if typ == bool: + if value is True: + value = 1 + elif value is False: + value = 0 + else: + raise PrefsDataTypeError, "%r is not a valid value for the key %r" % (value, name) + ret += "B %d %d " % (conf_bools[name]['index'], value) + elif typ == int: + value = int(value) + ret += "I %d %d " % (conf_ints[name]['index'], value) + elif typ == str: + value = str(value) + delms = list("\"'#?!%&*+=:;@~<>?,.{}[]()\xbb") + delm = None + while delm is None and len(delms) > 0: + if not delms[0] in value: + delm = delms[0] + else: + del delms[0] + if not delm: + raise UnableToFindADelimitersError, "Couldn't find a delminator for %r" % value + ret += "S %d %s%s%s " % (conf_strs[name]['index'], delm, value, delm) + else: + raise PrefsDataTypeError, "Unexpected record type for preferences %r" % typ + return ret + +# Header(3,14,'00000000000000'), +class TreeDisplayStatusHeader(Header): + """ Tree display status (what folders are expanded/collapsed + + """ + TYPE = 0x03 + + def __init__(self, htype=None, hlen=1, raw_data=None, status=''): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + self.status = status + + def parse(self): + """Parse data""" + self.status = self.data + + def __repr__(self): + return "Status" + Header.__repr__(self) + + def __str__(self): + return "Status=%r" % self.status + + def serial(self): + return self.status + +# Header(4,4,'Ao\xc8L'), + +# Header(6,19,'Password Safe V3.23'), +class LastSaveAppHeader(Header): + """ What app performed the last save + + """ + TYPE = 0x03 + + def __init__(self, htype=None, hlen=1, raw_data=None, status=''): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + self.status = status + + def parse(self): + """Parse data""" + self.status = self.data + + def __repr__(self): + return "Status" + Header.__repr__(self) + + def __str__(self): + return "Status=%r" % self.status + + def serial(self): + return self.status + +# Header(7,6,'owenst'), +# Header(8,15,'SOC01XENAPPMGR1'), + + + +class EOFHeader(Header): + """End of headers +>>> x=EOFHeader(255,0,'\x00\x00\x00\x00\xff\xbc_AP\x10\xf19\xae\xe99g') +>>> repr(x) +"EOFHeader(255,0,'\\x00\\x00\\x00\\x00\\xff\\xbc_AP\\x10\\xf19\\xae\\xe99g')" +>>> str(x) +'EOF' +>>> x.serial() +'' + + """ + TYPE = 0xff + data = '' + def __init__(self, htype=None, hlen=0, raw_data=''): + if not htype: + htype = self.TYPE + if raw_data: + Header.__init__(self, htype, hlen, raw_data) + else: + pass + + def __repr__(self): + return "EOF" + Header.__repr__(self) + + def __str__(self): + return "EOF" + +headers = { + 0x00:VersionHeader, + 0x01:UUIDHeader, + 0x02:NonDefaultPrefsHeader, + 0xFF:EOFHeader + } + +def Create_Header(fetchblock_f): + """Returns a header object. Uses fetchblock_f to read a 16 byte chunk of data + fetchblock_f(number of blocks) + """ + firstblock = fetchblock_f(1) + log.debug("Header of header: %s" % repr(firstblock[:5])) + (rlen, rtype) = unpack('=lc', firstblock[:5]) + rtype = ord(rtype) + data = firstblock[5:] + log.debug("Rtype: %s Len: %s" % (rtype, rlen)) + if rlen > len(data): + data += fetchblock_f(((rlen - len(data) - 1) / 16) + 1) + assert rlen <= len(data) + # TODO: Clean up header add back + data = firstblock[:5] + data + if headers.has_key(rtype): + return headers[rtype](rtype, rlen, data) + else: + # Unknown header + return Header(rtype, rlen, data) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/PWSafeV3Records.py b/PWSafeV3Records.py new file mode 100755 index 0000000..14f3550 --- /dev/null +++ b/PWSafeV3Records.py @@ -0,0 +1,1275 @@ +#!/usr/bin/env python +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== +"""Record and record properity objects + +""" + +from struct import unpack, pack +import calendar, time +import logging, logging.config +from errors import * +import os +from uuid import UUID, uuid4 +#logging.config.fileConfig('/etc/mss/psafe_log.conf') +psafe_logger = logging.getLogger("psafe.lib.record") +psafe_logger.debug('initing') + + +class Record(object): + """Represents a psafe3 record + Container item: Name of properity + Attrs + records [RecordProp] List of properities + lk {TypeName:RecordProp} + + """ + + def __init__(self, fetchblock_f=None): + psafe_logger.debug('Creating record object') + self.records = [] + self.lk = {} + if fetchblock_f: + psafe_logger.debug("Creating from existing data") + rcd = Create_Prop(fetchblock_f) + psafe_logger.debug("Creating record %s" % repr(rcd)) + self.records.append(rcd) + self.lk[rcd.rNAME] = rcd + while type(rcd) != EOERecordProp: + rcd = Create_Prop(fetchblock_f) + psafe_logger.debug("Creating record %s" % repr(rcd)) + self.records.append(rcd) + self.lk[rcd.rNAME] = rcd + psafe_logger.debug("Created all records. ") + else: + psafe_logger.debug("Creating blank object") + # Create the UUID object + self._if_noitem(UUIDRecordProp.rNAME) + # Create the EOE object. This must happen last and can't use the if_noitem because it may + # insert somewhere besides the end. + eoe = EOERecordProp() + self.records.append(eoe) + self.lk[eoe.rNAME] = eoe + psafe_logger.debug('Created record object. ') + + def __getitem__(self, key): + self._if_noitem(key) + return self.lk[key].get() + + def __setitem__(self, key, val): + self._if_noitem(key) + # atm we are just updating our lk record + self.lk[key].set(val) + + def _if_noitem(self, item): + """If an item isn't in our key store, create it. """ + if not self.lk.has_key(item): + for i in RecordPropTypes.values(): + if i.rNAME == item: + r = i() + self.lk[item] = r + self.records.insert(0, r) + + def __iter__(self): + return self.lk.__iter__() + + def __repr__(self): + ret = '' + for i in self: + ret += repr(self[i]) + "\n" + return ret[:-1] + + def __str__(self): + ret = '' + for i in self.lk.keys(): + #print str(self.lk[i]) + + ret += str(self.lk[i]) + "\n" + return ret[:-1] + + def __len__(self): + return len(self.records) + + def hmac_data(self): + """Returns the data required for the "broken" hmac in psafe3. See bug 1812081. """ + ret = '' + for i in self.records: + # the data field has the data minus the padding + #if i.serial()!=i.data: + # psafe_logger.warn('Serial != data for class %s. s: %s d: %s'%(repr(i.__class__),repr(i.serial()),repr(i.data))) + ret += i.serial() + return ret + + def serialiaze(self): + """ """ + ret = '' + for r in self.records: + ret += r.serialiaze() + return ret + +# Record Prop +class RecordProp(object): + """A single properity of a psafe3 record. This represents an unknown type or is overridden by records of a known type. + rTYPE int Properity type. May be null. + rNAME string Code name of properity type. + type int Prop type. + len int Length, in bytes, of data + raw_data string Record data including padding and headers + data string Record data minus headers and padding + """ + rTYPE = None + rNAME = "Unknown" + + def __init__(self, ptype, plen, pdata): + self.type = ptype + if self.rTYPE: + assert self.rTYPE == ptype + else: + self.rTYPE = ptype + self.len = plen + self.raw_data = pdata + self.data = pdata[5:(plen + 5)] + psafe_logger.debug('Created psafe record prop with rTYPE of %s. Class %s', repr(self.rTYPE), repr(self.__class__)) + self.parse() + if self.rNAME == "Unknown": + psafe_logger.warn('Created psafe record prop of unknown type. rType: %s rLen: %s Data: %s Raw: %s', repr(ptype), repr(plen), repr(self.data), repr(self.raw_data)) + else: + psafe_logger.debug('Created psafe record prop of known type. rType: %s rLen: %s Data: %s Raw: %s', repr(ptype), repr(plen), repr(self.data), repr(self.raw_data)) + + def parse(self): + """Override me. Called on init to parse received data. """ + pass + + def __repr__(self): + s = self.serial() + return "RecordProp(%s,%d,%s)" % (hex(self.type), len(s), repr(s)) + + def __str__(self): + return self.__repr__() + + def get(self): + return self.data + + def set(self, value): + self.data = value + + def _rand_char(self): + """Returns a random char""" + return os.urandom(1) + + def _pad(self, data): + """ Pad out data to 16 bytes """ + add_data = 16 - len(data) % 16 + if add_data == 16: + add_data = 0 + padding = '' + for i in xrange(0, add_data): + padding += self._rand_char() + assert add_data != 16 + assert len(padding) == add_data + assert len(data + padding) % 16 == 0 + return data + padding + + def serial(self): + """Returns the raw data blocks to generate this object + EXCLUDING TYPE+LEN!""" + #psafe_logger.debug('Serial to %s',repr(self.data)) + return self.data + + def serialiaze(self): + """Returns the raw data blocks to generate this object. """ + serial = self.serial() + header = pack('=lc', len(serial), chr(self.rTYPE)) + padded = self._pad(header + serial) + psafe_logger.debug("Padded output %s" % repr(padded)) + return padded + +class UUIDRecordProp(RecordProp): + """Record's unique id + uuid uuid.UUID +>>> x=UUIDRecordProp(0x1,16,'\x10\x00\x00\x00\x01\xfa@\xbd\x1f \xf5B\xf5\x88v#\xe4\x08\xae\x8a\xa1@\x16[\xfb\x8c\x87mq\xf70[') +>>> repr(x) +"UUIDRecordProp(0x1,16,'\\x10\\x00\\x00\\x00\\x01\\xfa@\\xbd\\x1f \\xf5B\\xf5\\x88v#\\xe4\\x08\\xae\\x8a\\xa1@\\x16[\\xfb\\x8c\\x87mq\\xf70[')" +>>> str(x) +'UUID=fa40bd1f-20f5-42f5-8876-23e408ae8aa1' +>>> x.serial() +'\xfa@\xbd\x1f \xf5B\xf5\x88v#\xe4\x08\xae\x8a\xa1' + """ + rTYPE = 0x01 + rNAME = 'UUID' + def __init__(self, ptype=None, plen=16, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.uuid = uuid4() + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.uuid = UUID(bytes=unpack('=16s', self.data)[0]) + + def __repr__(self): + return "UUID" + RecordProp.__repr__(self) + + def __str__(self): + return "UUID=%s" % str(self.uuid) + + def get(self): + return self.uuid + + def set(self, value): + """Accepts a UUID object or a hex string + """ + if type(value) == UUID: + self.uuid = value + else: + self.uuid = UUID(fields=value) + + def serial(self): + #psafe_logger.debug("Serial to %s",repr(pack('=16s',str(self.uuid.bytes)))) + return pack('=16s', str(self.uuid.bytes)) + +class GroupRecordProp(RecordProp): + """Record's Group + group_str string Raw group string + group [string] List of the groups. First entry is the top level group. +>>> x=GroupRecordProp(0x2,7,'\x07\x00\x00\x00\x02Group 0\x11"\xf1\x84') +>>> str(x) +"Group: 'Group 0'" +>>> repr(x) +'GroupRecordProp(0x2,7,\'\\x07\\x00\\x00\\x00\\x02Group 0\\x11"\\xf1\\x84\')' +>>> x.get() +['Group 0'] +>>> x.serial() +'\x07\x00\x00\x00\x02Group 0' + """ + rTYPE = 0x02 + rNAME = 'Group' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.group = [] + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.group_str = self.data + self.group = self.group_str.split('.') + + def __repr__(self): + return "Group" + RecordProp.__repr__(self) + + def __str__(self): + return "Group: %s" % repr(self.group_str) + + def get(self): + return self.group + + def set(self, value): + self.group = value + + def serial(self): + self.group_str = '.'.join(self.group) + #psafe_logger.debug("Serial to %s Data %s"%(repr(self.group_str),repr(self.data))) + return self.group_str + +class TitleRecordProp(RecordProp): + """Record's title + title string Title +>>> x=TitleRecordProp(0x3,7,'\x07\x00\x00\x00\x03Title 0\xd5\xed\xf5l') +>>> str(x) +"Title='Title 0'" +>>> repr(x) +"TitleRecordProp(0x3,7,'\\x07\\x00\\x00\\x00\\x03Title 0\\xd5\\xed\\xf5l')" +>>> x.serial() +'Title 0' +>>> x.get() +'Title 0' + + """ + rTYPE = 0x03 + rNAME = 'Title' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.title = '' + pdata = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.title = self.data[:self.len] + + def __repr__(self): + return "Title" + RecordProp.__repr__(self) + + def __str__(self): + return "Title=" + repr(self.title) + + def get(self): + return self.title + + def set(self, value): + self.title = str(value) + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.title),repr(self.data))) + return self.title + +class UsernameRecordProp(RecordProp): + """Record's username + username string ... +>>> x=UsernameRecordProp(0x4,9,'\t\x00\x00\x00\x04username0\x06\xfb') +>>> str(x) +"Username='username0'" +>>> repr(x) +"UsernameRecordProp(0x4,9,'\\t\\x00\\x00\\x00\\x04username0\\x06\\xfb')" +>>> x.get() +'username0' +>>> x.serial() +'username0' + """ + rTYPE = 0x04 + rNAME = 'Username' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + pdata = '' + self.username = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.username = self.data[:self.len] + + def __repr__(self): + return "Username" + RecordProp.__repr__(self) + + def __str__(self): + return "Username=" + repr(self.username) + + def get(self): + return self.username + + def set(self, value): + self.username = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.username),repr(self.data))) + return self.username + +class NotesRecordProp(RecordProp): + """Record notes + notes string ... +>>> x=NotesRecordProp(0x5,10,'\n\x00\x00\x00\x05more notes\x8c') +>>> str(x) +"Notes='more notes'" +>>> repr(x) +"NotesRecordProp(0x5,10,'\\n\\x00\\x00\\x00\\x05more notes\\x8c')" +>>> x.get() +'more notes' +>>> x.serial() +'more notes' + + """ + rTYPE = 0x05 + rNAME = 'Notes' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.notes = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.notes = self.data[:self.len] + + def __repr__(self): + return "Notes" + RecordProp.__repr__(self) + + def __str__(self): + return "Notes=" + repr(self.notes) + + def get(self): + return self.notes + + def set(self, value): + self.notes = str(value) + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.notes),repr(self.data))) + return self.notes + +class PasswordRecordProp(RecordProp): + """Record's password + password string ... +>>> x=PasswordRecordProp(0x6,9,'\t\x00\x00\x00\x06password0d\xe7') +>>> str(x) +"Password='password0'" +>>> repr(x) +"PasswordRecordProp(0x6,9,'\\t\\x00\\x00\\x00\\x06password0d\\xe7')" +>>> x.get() +'password0' +>>> x.serial() +'password0' + """ + rTYPE = 0x06 + rNAME = 'Password' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.password = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + # TODO: Add handling for links. See formatv3 3.3[3] + def parse(self): + self.password = self.data[:self.len] + + def __repr__(self): + return "Password" + RecordProp.__repr__(self) + + def __str__(self): + return "Password=" + repr(self.password) + + def get(self): + return self.password + + def set(self, value): + self.password = str(value) + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.password),repr(self.data))) + return self.password + +class CreationTimeRecordProp(RecordProp): + """Record's ctime + password string ... +>>> x=CreationTimeRecordProp(0x7,4,'\x04\x00\x00\x00\x07\xda\x17;G\x86\xd5\x7f\xd2\x1a\xeb\xc5') +>>> x.serial() +'\xda\x17;G' +>>> x.get() +(2007, 11, 14, 15, 44, 26, 2, 318, 0) +>>> str(x) +'CTime=Wed, 14 Nov 2007 15:44:26 +0000' +>>> repr(x) +"CreationTimeRecordProp(0x7,4,'\\x04\\x00\\x00\\x00\\x07\\xda\\x17;G\\x86\\xd5\\x7f\\xd2\\x1a\\xeb\\xc5')" + + """ + rTYPE = 0x07 + rNAME = 'ctime' + + def __init__(self, ptype=None, plen=4, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.dt = time.gmtime() + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.dt = parsedatetime(self.data[:self.len]) + + def __repr__(self): + return "CreationTime" + RecordProp.__repr__(self) + + def __str__(self): + return "CTime=" + time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.dt) + + def get(self): + return self.dt + + def set(self, value): + self.dt = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(makedatetime(self.dt)),repr(self.data))) + return makedatetime(self.dt) + +class ModTimeRecordProp(RecordProp): + """Record's mtime + password string ... +>>> x=ModTimeRecordProp(0x8,4,'\x04\x00\x00\x00\x08\xd6\x8apI\xd2\xec\xc0_\xfb\x994') +>>> x.serial() +'\xd6\x8apI' +>>> x.get() +(2009, 1, 16, 13, 25, 42, 4, 16, 0) +>>> str(x) +'MTime=Fri, 16 Jan 2009 13:25:42 +0000' +>>> repr(x) +"ModTimeRecordProp(0x8,4,'\\x04\\x00\\x00\\x00\\x08\\xd6\\x8apI\\xd2\\xec\\xc0_\\xfb\\x994')" + """ + rTYPE = 0x08 + rNAME = 'mtime' + + def __init__(self, ptype=None, plen=4, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.dt = time.gmtime() + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.dt = parsedatetime(self.data[:self.len]) + + def __repr__(self): + return 'ModTime' + RecordProp.__repr__(self) + + def __str__(self): + return "MTime=" + time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.dt) + + def get(self): + return self.dt + + def set(self, value): + self.dt = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(makedatetime(self.dt)),repr(self.data))) + return makedatetime(self.dt) + +class LastAccessTimeRecordProp(RecordProp): + """Record's ctime + password string ... + +>>> x=LastAccessTimeRecordProp(0x9,4,'\x04\x00\x00\x00\t\xe4\x9fpI\xb2H\x860\x7f|\xf8') +>>> str(x) +'LastAccess=Fri, 16 Jan 2009 14:55:32 +0000' +>>> repr(x) +"LastAccessTimeRecordProp(0x9,4,'\\x04\\x00\\x00\\x00\\t\\xe4\\x9fpI\\xb2H\\x860\\x7f|\\xf8')" +>>> x.get() +(2009, 1, 16, 14, 55, 32, 4, 16, 0) +>>> x.serial() +'\xe4\x9fpI' + + """ + rTYPE = 0x09 + rNAME = 'LastAccess' + + def __init__(self, ptype=None, plen=4, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.dt = time.gmtime() + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.dt = parsedatetime(self.data[:self.len]) + + def __repr__(self): + return 'LastAccessTime' + RecordProp.__repr__(self) + + def __str__(self): + return self.rNAME + "=" + time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.dt) + + def get(self): + return self.dt + + def set(self, value): + self.dt = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(makedatetime(self.dt)),repr(self.data))) + return makedatetime(self.dt) + +class PasswordExpiryTimeRecordProp(RecordProp): + """Record's experation time + password string ... +>>> x=PasswordExpiryTimeRecordProp(0xa,4,'\x04\x00\x00\x00\n""wIy\xb8La\x8fOu') +>>> str(x) +'PasswordExpiry=Wed, 21 Jan 2009 13:24:50 +0000' +>>> repr(x) +'PasswordExpiryTimeRecordProp(0xa,4,\'\\x04\\x00\\x00\\x00\\n""wIy\\xb8La\\x8fOu\')' +>>> x.get() +(2009, 1, 21, 13, 24, 50, 2, 21, 0) +>>> x.serial() +'""wI' + """ + rTYPE = 0x0a + rNAME = 'PasswordExpiry' + + def __init__(self, ptype=None, plen=4, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.dt = time.gmtime(0) + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.dt = parsedatetime(self.data[:self.len]) + + def __repr__(self): + return 'PasswordExpiryTime' + RecordProp.__repr__(self) + + def __str__(self): + return self.rNAME + "=" + time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.dt) + + def get(self): + return self.dt + + def set(self, value): + self.dt = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(makedatetime(self.dt)),repr(self.data))) + return makedatetime(self.dt) + +class LastModificationTimeRecordProp(RecordProp): + """Record's experation time + password string ... +>>> x=LastModificationTimeRecordProp(0xc,4,'\x04\x00\x00\x00\x0c\xd6\x8apI\x14\xff\xb1?\x80q\xeb') +>>> str(x) +'LastModification=Fri, 16 Jan 2009 13:25:42 +0000' +>>> repr(x) +"LastModificationTimeRecordProp(0xc,4,'\\x04\\x00\\x00\\x00\\x0c\\xd6\\x8apI\\x14\\xff\\xb1?\\x80q\\xeb')" +>>> x.get() +(2009, 1, 16, 13, 25, 42, 4, 16, 0) +>>> x.serial() +'\xd6\x8apI' + + """ + rTYPE = 0x0c + rNAME = 'LastModification' + + def __init__(self, ptype=None, plen=4, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.dt = time.gmtime() + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.dt = parsedatetime(self.data[:self.len]) + + def __repr__(self): + return 'LastModificationTime' + RecordProp.__repr__(self) + + def __str__(self): + return self.rNAME + "=" + time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.dt) + + def get(self): + return self.dt + + def set(self, value): + self.dt = value + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(makedatetime(self.dt)),repr(self.data))) + return makedatetime(self.dt) + +class URLRecordProp(RecordProp): + """Record's URL + title string Title +>>> x=URLRecordProp(0xd,5,'\x05\x00\x00\x00\ra url\xaczf\xca:2') +>>> repr(x) +"URLRecordProp(0xd,5,'\\x05\\x00\\x00\\x00\\ra url\\xaczf\\xca:2')" +>>> str(x) +"URL='a url'" +>>> x.get() +'a url' +>>> x.serial() +'a url' + """ + rTYPE = 0x0d + rNAME = 'URL' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.url = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.url = self.data[:self.len] + + def __repr__(self): + return 'URL' + RecordProp.__repr__(self) + + def __str__(self): + return self.rNAME + "=" + repr(self.url) + + def get(self): + return self.url + + def set(self, value): + self.url = str(value) + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.url),repr(self.data))) + return self.url + +class AutotypeRecordProp(RecordProp): + """Record's title + title string Title +>>> x=AutotypeRecordProp(0xe,12,'\x0c\x00\x00\x00\x0emeh autotype-Ch\xcd\x99$i\xc0\xb7\x87\x0f\x0bN,\x05') +>>> repr(x) +"AutotypeRecordProp(0xe,12,'\\x0c\\x00\\x00\\x00\\x0emeh autotype-Ch\\xcd\\x99$i\\xc0\\xb7\\x87\\x0f\\x0bN,\\x05')" +>>> str(x) +"AutoType='meh autotype'" +>>> x.get() +'meh autotype' +>>> x.serial() +'meh autotype' + + """ + rTYPE = 0x0e + rNAME = 'Autotype' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.autotype = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.autotype = self.data[:self.len] + + def __repr__(self): + return "Autotype" + RecordProp.__repr__(self) + + def __str__(self): + return "AutoType=" + repr(self.autotype) + + def get(self): + return self.autotype + + def set(self, value): + self.autotype = str(value) + + def serial(self): + #psafe_logger.debug("Serial to %s data %s"%(repr(self.autotype),repr(self.data))) + return self.autotype + +class PasswordHistoryRecordProp(RecordProp): + """Record's old passwords +Password History is an optional record. If it exists, it stores the +creation times and values of the last few passwords used in the current +entry, in the following format: + "fmmnnTLPTLP...TLP" +where: + f = {0,1} if password history is on/off + mm = 2 hexadecimal digits max size of history list (i.e. max = 255) + nn = 2 hexadecimal digits current size of history list + T = Time password was set (time_t written out in %08x) + L = 4 hexadecimal digit password length (in TCHAR) + P = Password + + raw_data string Just my data + enabled bool True if we are keeping password history + history [time,pass] Password history + maxsize int Max size of history + zerohack bool True if there is a trailing 00. Used + to make sure the hmac calcs right on + certian records with no history. + ie does 0ff0000 instead of 0ff00. + Update: See bug 2529736. + +>>> x=PasswordHistoryRecordProp(0xf,72,'H\x00\x00\x00\x0f1ff03473b17da000blaskdjflkxn48d98049000claskdjflkxnd49708a980008N0Y2Dkir\x9b\xa4\xb5') +>>> x.serial() +'1ff03473b17da000blaskdjflkxn48d98049000claskdjflkxnd49708a980008N0Y2Dkir' +>>> repr(x) +"PasswordHistoryRecordProp(0xf,72,'H\\x00\\x00\\x00\\x0f1ff03473b17da000blaskdjflkxn48d98049000claskdjflkxnd49708a980008N0Y2Dkir\\x9b\\xa4\\xb5')" +>>> str(x) +'PasswordHistory(Enabled: True\nMax size: 255\nCur Size: 3\nCreated: Wed, 14 Nov 2007 15:44:26 +0000\nPassword: laskdjflkxn\nCreated: Tue, 23 Sep 2008 23:48:25 +0000\nPassword: laskdjflkxnd\nCreated: Fri, 16 Jan 2009 13:24:40 +0000\nPassword: N0Y2Dkir\n)' +>>> x.get() +{'maxsize': 255, 'enable': True, 'history': {'Tue, 23 Sep 2008 23:48:25 +0000': 'laskdjflkxnd', 'Fri, 16 Jan 2009 13:24:40 +0000': 'N0Y2Dkir', 'Wed, 14 Nov 2007 15:44:26 +0000': 'laskdjflkxn'}, 'currentsize': 3} + + """ + rTYPE = 0x0f + rNAME = 'PasswordHistory' + + def __init__(self, ptype=None, plen=0, pdata=None, enabled=0, maxsize=255): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.enabled = enabled + self.maxsize = maxsize + self.history = [] + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def serial(self): + ret = '' + if self.enabled: + ret += "1" + else: + ret += "0" + ret += "%02x" % self.maxsize + ret += "%02x" % len(self.history) + psafe_logger.debug("Pre-passwords %s" % repr(ret)) + for (tm, passwd) in self.history: + ret += "%08x" % calendar.timegm(tm) + ret += "%04x" % len(passwd) + ret += passwd + psafe_logger.debug("Post-add password %s" % repr(ret)) + if len(self.history) == 0 and self.zerohack: + ret += "00" + #psafe_logger.debug("Serial to %s data %s"%(repr(ret),repr(self.data))) + return ret + + def parse(self): + if len(self.data) == 7: + self.zerohack = True + else: + self.zerohack = False + if len(self.data) > 0: + # Enabled/disabled + if self.data[0] == "0": + self.enabled = False + elif self.data[0] == "1": + self.enabled = True + else: + raise PropParsingError, "Invalid enabled/disabled flag %s" % repr(self.data[0]) + psafe_logger.debug("Set password history to %s", repr(self.enabled)) + # Max size of hist list + try: + self.maxsize = int(self.data[1:3], 16) + except ValueError: + raise PropParsingError, "Invalid maxsize type %s" % repr(self.data[1:3]) + if self.maxsize < 0 or self.maxsize > 255: + raise PropParsingError, "Invalid maxsize value %s" % repr(self.data[1:3]) + psafe_logger.debug("Set password history max size to %d", self.maxsize) + # Current size of hist list + try: + self._cursize = int(self.data[3:5], 16) + except ValueError: + raise PropParsingError, "Invalid cursize type %s" % repr(self.data[3:5]) + if self._cursize < 0 or self._cursize > 255: + raise PropParsingError, "Invalid cursize value %s" % repr(self.data[3:5]) + psafe_logger.debug("Set current size of password history to %d", self._cursize) + # FIXME: Should this end at self.len? + try: + data = self.data[5:] + self.history = [] + i = self._cursize + while len(data) > 12 and i > 0: + i -= 1 + # Split out the data + tm_raw = data[:8] + len_raw = data[8:12] + data = data[12:] + # Parse known data + # TODO: Check for errors + tm = time.gmtime(int(tm_raw, 16)) + len_real = int(len_raw, 16) + password = data[:len_real] + data = data[len_real:] + self.history.append((tm, password)) + except ValueError: + raise PropParsingError, "Error parsing password history" + assert self._cursize == len(self.history) + + def __repr__(self): + return self.rNAME + RecordProp.__repr__(self) + + def __str__(self): + ret = self.rNAME + "(" + ret += "Enabled: %s\n" % repr(self.enabled) + ret += "Max size: %s\n" % repr(self.maxsize) + ret += "Cur Size: %s\n" % repr(len(self.history)) + for (createddt, passwd) in self.history: + ret += "Created: %s\n" % time.strftime("%a, %d %b %Y %H:%M:%S +0000", createddt) + ret += "Password: %s\n" % passwd + ret += ")" + return ret + + def get(self): + hist = {} + for (createddt, passwd) in self.history: + #used to return time.strftime("%a, %d %b %Y %H:%M:%S +0000",createddt) + hist[createddt] = passwd + return dict( + enable=self.enabled + , maxsize=self.maxsize + , currentsize=len(self.history) + , history=hist + ) + + def set(self, value): + self.enabled = value['enabled'] + self.maxsize = value['maxsize'] + self.history = [] + for (dt, passwd) in value['history']: + self.history.append((dt, passwd)) + +class PasswordPolicyRecordProp(RecordProp): + """Record's title + This field allows a specific Password Policy per entry. The format is: + ffffnnnllluuudddsss" +where: + ffff = 4 hexadecimal digits representing the following flags + UseLowercase = 0x8000 - can have a minimum length + UseUppercase = 0x4000 - can have a minimum length + UseDigits = 0x2000 - can have a minimum length + UseSymbols = 0x1000 - can have a minimum length + UseHexDigits = 0x0800 (if set, then no other flags can be set) + UseEasyVision = 0x0400 + MakePronounceable = 0x0200 + Unused 0x01ff + nnn = 3 hexadecimal digits password total length + lll = 3 hexadecimal digits password minimum number of lowercase characters + uuu = 3 hexadecimal digits password minimum number of uppercase characters + ddd = 3 hexadecimal digits password minimum number of digit characters + sss = 3 hexadecimal digits password minimum number of symbol characters + +>>> x=PasswordPolicyRecordProp(0x10,19,'\x13\x00\x00\x00\x10f000010004001005002n_S-\x84r\xeb\xe4') +>>> str(x) +'PasswordPolicy=\nUse Lowercase: True\nUse Uppercase: True\nUse Digits: True\nUse Symbols: True\nUse Hex: False\nUse Easy Version: False\nMake Pronounceable: False\nTotal Length: 16\nMin Lowercase: 4\nMin Uppercase: 1\nMin Digits: 5\nMin Symbols: 2' +>>> repr(x) +"PasswordPolicyRecordProp(0x10,19,'\\x13\\x00\\x00\\x00\\x10f000010004001005002n_S-\\x84r\\xeb\\xe4')" +>>> x.get() +{'UseEasy': False, 'MinSymbols': 2, 'MinDigits': 5, 'MakePronounceable': False, 'MinUpper': 1, 'UseHex': False, 'TotalLen': 16, 'UseDigits': True, 'UseLower': True, 'UseSymbols': True, 'MinLower': 4, 'UseUpper': True} +>>> x.serial() +'b000010004001005002' + """ + rTYPE = 0x10 + rNAME = 'PasswordPolicy' + # A few constants + USELOWERCASE = 0x8000 + USEUPPERCASE = 0x4000 + USEDIGITS = 0x2000 + USESYMBOLS = 0x1000 + USEHEXDIGITS = 0x0800 + USEEASYVERSION = 0x0400 + MAKEPRONOUNCEABLE = 0x0200 + UNUSED = 0x01ff + + def __init__(self + , ptype=None + , plen=0 + , pdata=None + , ttllen=14 + , minlow=2 + , minup=2 + , mindig=2 + , minsym=2 + , uselowercase=True + , useuppercase=True + , usedigits=True + , usesymbols=True + , usehex=False + , useeasy=False + , makepron=False + ): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.ttllen = ttllen + self.minlow = minlow + self.minup = minup + self.mindig = mindig + self.minsym = minsym + self.uselowercase = uselowercase + self.useuppercase = useuppercase + self.usedigits = usedigits + self.usesymbols = usesymbols + self.usehex = usehex + self.useeasy = useeasy + self.makepron = makepron + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def serial(self): + flags = 0 + if self.uselowercase: + flags = flags | self.USELOWERCASE + if self.useuppercase: + flags = flags | self.USELOWERCASE + if self.usedigits: + flags = flags | self.USEDIGITS + if self.usesymbols: + flags = flags | self.USESYMBOLS + if self.usehex: + flags = flags | self.USEHEXDIGITS + if self.useeasy: + flags = flags | self.USEEASYVERSION + if self.makepron: + flags = flags | self.MAKEPRONOUNCEABLE + ret = '%04x%03x%03x%03x%03x%03x' % (flags, self.ttllen, self.minlow, self.minup, self.mindig, self.minsym) + #psafe_logger.debug("Serial to %s data %s"%(repr(ret),repr(self.data))) + return ret + + def parse(self): + self.mydata = self.data[:self.len] + policy = unpack('=4s3s3s3s3s3s', self.mydata) + # str hex to int + policy = [int(x, 16) for x in policy] + (flags, self.ttllen, self.minlow, self.minup, self.mindig, self.minsym) = policy + if flags & self.USELOWERCASE: + self.uselowercase = True + else: + self.uselowercase = False + if flags & self.USEUPPERCASE: + self.useuppercase = True + else: + self.useuppercase = False + if flags & self.USEDIGITS: + self.usedigits = True + else: + self.usedigits = False + if flags & self.USESYMBOLS: + self.usesymbols = True + else: + self.usesymbols = False + if flags & self.USEHEXDIGITS: + self.usehex = True + else: + self.usehex = False + if flags & self.USEEASYVERSION: + self.useeasy = True + else: + self.useeasy = False + if flags & self.MAKEPRONOUNCEABLE: + self.makepron = True + else: + self.makepron = False + psafe_logger.debug(str(self)) + + def __repr__(self): + return self.rNAME + RecordProp.__repr__(self) + + def __str__(self): + ret = self.rNAME + "=" + ret += "\nUse Lowercase: " + str(self.uselowercase) + ret += "\nUse Uppercase: " + str(self.useuppercase) + ret += "\nUse Digits: " + str(self.usedigits) + ret += "\nUse Symbols: " + str(self.usesymbols) + ret += "\nUse Hex: " + str(self.usehex) + ret += "\nUse Easy Version: " + str(self.useeasy) + ret += "\nMake Pronounceable: " + str(self.makepron) + ret += "\nTotal Length: " + str(self.ttllen) + ret += "\nMin Lowercase: " + str(self.minlow) + ret += "\nMin Uppercase: " + str(self.minup) + ret += "\nMin Digits: " + str(self.mindig) + ret += "\nMin Symbols: " + str(self.minsym) + return ret + + def get(self): + return dict( + UseLower=self.uselowercase + , UseUpper=self.useuppercase + , UseDigits=self.usedigits + , UseSymbols=self.usesymbols + , UseHex=self.usehex + , UseEasy=self.useeasy + , MakePronounceable=self.makepron + , TotalLen=self.ttllen + , MinLower=self.minlow + , MinUpper=self.minup + , MinDigits=self.mindig + , MinSymbols=self.minsym + ) + + def set(self, value): + self.uselowercase = bool(value['UseLower']) + self.useuppercase = bool(value['UseUpper']) + self.usedigits = bool(value['UseDigits']) + self.usesymbols = bool(value['UseSymbols']) + self.usehex = bool(value['UseHex']) + self.useeasy = bool(value['UseEasy']) + self.makepron = bool(value['MakePronounceable']) + self.ttllen = int(value['TotalLen']) + self.minlow = int(value['MinLower']) + self.minup = int(value['MinUpper']) + self.mindig = int(value['MinDigits']) + self.minsym = int(value['MinSymbols']) + +class PasswordExpiryIntervalRecordProp(RecordProp): + """Number of days before the password expires. +Password Expiry Interval, in days, before this password expires. Once set, +this value is used when the password is first generated and thereafter whenever +the password is changed, until this value is unset. Valid values are 1-3650 +corresponding to up to approximately 10 years. A value of zero is equivalent to +this field not being set. + + ttl string Title +>>> x=PasswordExpiryIntervalRecordProp(0x11,4,'\x04\x00\x00\x00\x11\n\x00\x00\x00\xf1\xe9\xb1\xd0e\xd6h') +>>> repr(x) +"PasswordExpiryIntervalRecordProp(0x11,4,'\\x04\\x00\\x00\\x00\\x11\\n\\x00\\x00\\x00\\xf1\\xe9\\xb1\\xd0e\\xd6h')" +>>> str(x) +'PasswordExpiryInterval=10' +>>> x.get() +10 +>>> x.serial() +'\n\x00\x00\x00' + """ + rTYPE = 0x11 + rNAME = 'PasswordExpiryInterval' + + def __init__(self, ptype=None, plen=4, pdata=None, ttl=0): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + self.ttl = ttl + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def parse(self): + self.mydata = self.data[:self.len] + self.ttl = int(unpack('=l', self.mydata)[0]) + + def __repr__(self): + return self.rNAME + RecordProp.__repr__(self) + + def __str__(self): + return self.rNAME + "=" + repr(self.ttl) + + def get(self): + return self.ttl + + def set(self, value): + self.ttl = int(value) + + def serial(self): + ret = pack('=l', self.ttl) + #psafe_logger.debug("Serial to %s data %s"%(repr(ret),repr(self.data))) + return ret + +class EOERecordProp(RecordProp): + """End of entry + +>>> x=EOERecordProp(0xff,0,'\x00\x00\x00\x00\xff\xb5\xce\xd9 =\xe99\x14\xc1.\xfe') +>>> repr(x) +"EOERecordProp(0xff,0,'\\x00\\x00\\x00\\x00\\xff\\xb5\\xce\\xd9 =\\xe99\\x14\\xc1.\\xfe')" +>>> str(x) +'End of Entry' +>>> x.get() +'EOE' +>>> x.serial() +'\x00\x00\x00\x00\xff\xb5\xce\xd9 =\xe99\x14\xc1.\xfe' + """ + rTYPE = 0xff + rNAME = 'EOE' + + def __init__(self, ptype=None, plen=0, pdata=None): + if not ptype: + ptype = self.rTYPE + assert ptype == self.rTYPE + if not pdata: + pdata = '' + else: + RecordProp.__init__(self, ptype, plen, pdata) + + def __repr__(self): + return "EOE" + RecordProp.__repr__(self) + + def __str__(self): + return "End of Entry" + + def get(self): + return '' + + def set(self, value): + raise ValueError, "Can't set data to the EOE record" + + def serial(self): + return '' + +def parsedatetime(data): + """Takes in the raw psafev3 data for a time value and returns a date/time tuple""" + return time.gmtime(unpack('=i', data)[0]) + +def makedatetime(dt): + return pack('=i', calendar.timegm(dt)) + +RecordPropTypes = { + 0x01:UUIDRecordProp + , 0x02:GroupRecordProp + , 0x03:TitleRecordProp + , 0x04:UsernameRecordProp + , 0x05:NotesRecordProp + , 0x06:PasswordRecordProp + , 0x07:CreationTimeRecordProp + , 0x08:ModTimeRecordProp + , 0x09:LastAccessTimeRecordProp + , 0x0a:PasswordExpiryTimeRecordProp + , 0x0c:LastModificationTimeRecordProp + , 0x0d:URLRecordProp + , 0x0e:AutotypeRecordProp + , 0x0f:PasswordHistoryRecordProp + , 0x10:PasswordPolicyRecordProp + , 0x11:PasswordExpiryIntervalRecordProp + , 0xff:EOERecordProp +} +def Create_Prop(fetchblock_f): + """Returns a record properity. Uses fetchblock_f to read a 16 byte chunk of data + fetchblock_f(number of blocks) + """ + psafe_logger.debug('Create_Prop') + firstblock = fetchblock_f(1) + (rlen, rTYPE) = unpack('=lc', firstblock[:5]) + rTYPE = ord(rTYPE) + psafe_logger.debug('rtype %s rlen %s' % (rTYPE, rlen)) + data = firstblock[5:] + if rlen > len(data): + data += fetchblock_f(((rlen - len(data) - 1) / 16) + 1) + assert rlen <= len(data) + #print "Creating records with %s"%repr((rTYPE,rlen,data,len(data))) + # Lazy way to add the header data back + # TODO: Clean up header add back + data = firstblock[:5] + data + if RecordPropTypes.has_key(rTYPE): + try: + return RecordPropTypes[rTYPE](rTYPE, rlen, data) + except: + psafe_logger.exception('Failed to create record prop') + return RecordProp(rTYPE, rlen, data) + else: + # Unknown header + psafe_logger.info('Unknown header type %s' % repr(rTYPE)) + return RecordProp(rTYPE, rlen, data) + +if __name__ == "__main__": + import doctest + doctest.testmod() + diff --git a/README b/README new file mode 100644 index 0000000..958c892 --- /dev/null +++ b/README @@ -0,0 +1,23 @@ +A pure-Python library that can read and write Password Safe v3 files has been released under the GPLv2 by Symantec. Attached is a copy of the latest code. + +Id like to either host this as a sub-project out of the Password Safe project or create a new SourceForge/Google Code project for this library. Thoughts? + +There are a few known issues: + +1) HMAC validation fails with some recent versions of Password Safe. I suspect this is an issue with the support for bug 1812081. +2) Some of the more recent record properties arent supported +3) Lack of documentation + +I intend to update the library to correct these issues as time allows. + +I can be reached with any thoughts, ideas, or concerns via paul at gpmidi net. + +Best Regards, + +Paulson McIntyre +Sr. Security Engineer +Managed Security Services +Symantec Corporation +www.symantec.com +Office: (571) 289 0935 + diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..8531ae1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== +""" + + +""" +# Lets this lib work from both 2.4 and above +try: + from hashlib import sha256_func #@UnresolvedImport + from hashlib import sha256_mod #@UnresolvedImport +except: + import Crypto.Hash.SHA256 as sha256_mod #@UnresolvedImport + from Crypto.Hash.SHA256 import new as sha256_func #@UnresolvedImport +from mcrypt import MCRYPT #@UnresolvedImport +from hmac import new as HMAC +from PWSafeV3Headers import * +from PWSafeV3Records import * +from errors import * +import os, os.path +from struct import pack, unpack +import logging, logging.config + +log = logging.getLogger("psafe.lib.init") +log.debug('initing') +from uuid import uuid4 + +def stretchkey(passwd, salt, count): + """Streach a key. H(pass+salt) + + """ + assert count > 0 + # Hash once with both + inithsh = sha256_func() + inithsh.update(passwd) + inithsh.update(salt) + # Expecting it in binary form; NOT HEX FORM + hsh = inithsh.digest() + # Rehash + for i in xrange(count): + t = sha256_func() + t.update(hsh) + hsh = t.digest() + return hsh + +from struct import pack, unpack +class PWSafe3(object): + """ + filename string Full path to pwsafe + password string Passsafe password + fl File Object PWSafe file handle + flfull string Contents of pwsafe file + pprime string Stretched key used in B1-B4 + enckey string K; session key for main data block + hshkey string L; hmac key + records [Record] List of all records we have + hmacreq [functions] List of functions to run to generate hmac. Order matters when reading a + file. + hmac string Originally its the hmac from the file. Should be updated when ever changes + are made. + mode string RO,RW + iv string(16) Initialization vector used for CBC mode when encrypting/decrypting the + header and records. + # Passsafe fields + version + uuid + prefs + + """ + def __init__(self, filename, password, mode="RW"): + log.debug('Creating psafe %s' % repr(filename)) + psafe_exists = os.access(filename, os.F_OK) + psafe_canwrite = os.access(filename, os.W_OK) + psafe_canwritebase = os.access(os.path.dirname(filename), os.W_OK) + psafe_canread = os.access(filename, os.R_OK) + if psafe_exists and psafe_canread and not psafe_canwrite: + log.debug("Opening RO") + self.mode = "RO" + elif psafe_exists and psafe_canread and psafe_canwrite and mode == "RW": + log.debug("Opening RW") + self.mode = "RW" + elif psafe_exists and psafe_canread and psafe_canwrite and mode != "RW": + log.debug("Opening RO") + self.mode = "RO" + elif not psafe_exists and psafe_canwritebase and mode == "RW": + log.debug("Creating new psafe as RW") + self.mode = "RW" + elif not psafe_exists and psafe_canwrite and mode != "RW": + log.warn("Asked to create a new psafe but mode is set to RO") + raise AccessError, "Asked to create a new safe in RO mode" + elif psafe_exists: + log.warn("Can't read safe %s" % repr(filename)) + raise AccessError, "Can't read %s" % filename + else: + log.warn("Safe doesn't exist or can't read directory") + raise AccessError, "No such safe %s" % filename + if psafe_exists: + log.debug("Loading existing safe") + self.filename = filename + self.fl = open(self.filename, 'r') + try: + self.flfull = self.fl.read() + self.password = str(password) + # Read in file + self.load() + finally: + self.fl.close() + else: + log.debug("New psafe") + self.password = str(password) + self.filename = filename + # Init local vars + # SALT + self.salt = os.urandom(32) + log.debug("Salt is %s" % repr(self.salt)) + # ITER + self.iter = pow(2, 11) #2048 + log.debug("Iter set to %s" % self.iter) + # K + self.enckey = os.urandom(32) + # L + self.hshkey = os.urandom(32) + # IV + self.iv = os.urandom(16) + # Tag + self.tag = "PWS3" + # EOF + self.eof = "PWS3-EOFPWS3-EOF" + self.headers = [] + self.hmacreq = [] + self.records = [] + + def __len__(self): + return len(self.records) + + def save(self): + if self.mode == "RW": + self.serialiaze() + fil = open(self.filename, "w") + fil.write(self.flfull) + fil.close() + else: + raise ROSafe, "Safe is not in read/write mode" + + def serialiaze(self): + """ + + """ + # P' + self._regen_pprime() + # Regen b1b2 + self._regen_b1b2() + # Regen b3b4 + self._regen_b3b4() + # Regen H(P') + self._regen_hpprime() + # Regen hmac + self.hmac = self.current_hmac() + + log.debug('Loading psafe') + self.flfull = pack( + '4s32sI32s32s32s16s' + , self.tag + , self.salt + , self.iter + , self.hpprime + , self.b1b2 + , self.b3b4 + , self.iv + ) + log.debug("Pre-header flfull now %s", (self.flfull,)) + self.fulldata = '' + for header in self.headers: + self.fulldata += header.serialiaze() + #log.debug("In header flfull now %s",(self.flfull,)) + for record in self.records: + self.fulldata += record.serialiaze() + #log.debug("In record flfull now %s",(self.flfull,)) + # Encrypted self.fulldata to self.cryptdata + log.debug("Encrypting header/record data %s" % repr(self.fulldata)) + self.encrypt_data() + self.flfull += self.cryptdata + log.debug("Adding crypt data %s" % repr(self.cryptdata)) + self.flfull += pack('16s32s', self.eof, self.hmac) + log.debug("Post EOF flfull now %s", (self.flfull,)) + + def _regen_pprime(self): + """Regenerate P'. This is the stretched version of salt and password. """ + self.pprime = stretchkey(self.password, self.salt, self.iter) + log.debug("P'=%s" % repr(self.pprime)) + + def _regen_b1b2(self): + """Regenerate b1 and b2. This is the encrypted form of K. + + """ + tw = MCRYPT('twofish', 'ecb') + tw.init(self.pprime) + self.b1b2 = tw.encrypt(self.enckey) + log.debug("B1/B2 set to %s" % repr(self.b1b2)) + + def _regen_b3b4(self): + """Regenerate b3 and b4. This is the encrypted form of L. + """ + tw = MCRYPT('twofish', 'ecb') + tw.init(self.pprime) + self.b3b4 = tw.encrypt(self.hshkey) + log.debug("B3/B4 set to %s" % repr(self.b3b4)) + + def _regen_hpprime(self): + """Regenerate H(P') + Save the SHA256 of self.pprime. + """ + hsh = sha256_func() + hsh.update(self.pprime) + self.hpprime = hsh.digest() + log.debug("Set H(P') to %s" % repr(self.hpprime)) + assert self.check_password() + + def load(self): + """Load a psafe3 file + Will raise PasswordError if the password is bad. + Format: + Name Bytes Type + TAG 4 ASCII + SALT 32 BIN + ITER 4 INT 32 + H(P') 32 BIN + B1 16 BIN + B2 16 BIN + B3 16 BIN + B4 16 BIN + IV 16 BIN + Crypted 16n BIN + EOF 16 ASCII + HMAC 32 BIN + """ + log.debug('Loading psafe') + (self.tag, self.salt, self.iter, self.hpprime, self.b1b2, self.b3b4, self.iv) = unpack('4s32sI32s32s32s16s', self.flfull[:152]) + log.debug("Tag: %s" % repr(self.tag)) + log.debug("Salt: %s" % repr(self.salt)) + log.debug("Iter: %s" % repr(self.iter)) + log.debug("H(P'): %s" % repr(self.hpprime)) + log.debug("B1B2: %s" % repr(self.b1b2)) + log.debug("B3B4: %s" % repr(self.b3b4)) + log.debug("IV: %s" % repr(self.iv)) + self.cryptdata = self.flfull[152:-48] + (self.eof, self.hmac) = unpack('16s32s', self.flfull[-48:]) + log.debug("EOF: %s" % repr(self.eof)) + log.debug("HMAC: %s" % repr(self.hmac)) + # Determin the password hash + self.update_pprime() + # Verify password + if not self.check_password(): + raise PasswordError + # Figure out the encryption and hash session keys + self.calc_keys() + self.decrypt_data() + + # Parse headers + self.headers = [] + self.hmacreq = [] + self.remaining_headers = self.fulldata + hdr = Create_Header(self._fetch_block) + self.headers.append(hdr) + self.hmacreq.append(hdr.hmac_data) + #print str(hdr) +"--"+ repr(hdr) + while type(hdr) != EOFHeader: + hdr = Create_Header(self._fetch_block) + self.headers.append(hdr) + #print str(hdr) +"--"+ repr(hdr) + + # Parse DB + self.records = [] + while len(self.remaining_headers) > 0: + req = Record(self._fetch_block) + self.records.append(req) + + if self.current_hmac(cached=True) != self.hmac: + log.error('Invalid HMAC Calculated: %s File: %s' % (repr(self.current_hmac()), repr(self.hmac))) + #raise InvalidHMACError, "Calculated: %s File: %s"%(repr(self.current_hmac()),repr(self.hmac)) + + def __str__(self): + ret = '' + for i in self.records: + ret += str(i) + "\n\n" + return ret + + def _fetch_block(self, num_blocks=1): + """Returns one or more 16-byte block of data. Raises EOFError when there is no more data. """ + assert num_blocks > 0 + bytes = num_blocks * 16 + if bytes > len(self.remaining_headers): + raise EOFError, "No more header data" + ret = self.remaining_headers[:bytes] + self.remaining_headers = self.remaining_headers[bytes:] + return ret + + def calc_keys(self): + """Calculate sessions keys for encryption and hmac. Is based on pprime, b1b2, b3b4""" + tw = MCRYPT('twofish', 'ecb') + tw.init(self.pprime) + self.enckey = tw.decrypt(self.b1b2) + # its ok to reuse; ecb doesn't keep state info + self.hshkey = tw.decrypt(self.b3b4) + log.debug("Encryption key K: %s " % repr(self.enckey)) + log.debug("HMAC Key L: %s " % repr(self.hshkey)) + + def decrypt_data(self): + """Decrypt encrypted portion of header and data""" + tw = MCRYPT('twofish', 'cbc') + tw.init(self.enckey, self.iv) + self.fulldata = tw.decrypt(self.cryptdata) + + def encrypt_data(self): + """Encrypted fulldata to cryptdata""" + tw = MCRYPT('twofish', 'cbc') + tw.init(self.enckey, self.iv) + self.cryptdata = tw.encrypt(self.fulldata) + + def current_hmac(self, cached=False): + """Returns the current hmac of self.fulldata""" + data = '' + for i in self.headers: + log.debug("Adding hmac data %s", repr(i.hmac_data())) + if cached: + data += i.data + else: + data += i.hmac_data() + for i in self.records: + log.debug("Adding hmac data %s", repr(i.hmac_data())) + data += i.hmac_data() + log.debug("Building hmac with key %s", repr(self.hshkey)) + hm = HMAC(self.hshkey, data, sha256_mod) + #print hm.hexdigest() + log.debug("HMAC %s-%s", repr(hm.hexdigest()), repr(hm.digest())) + return hm.digest() + + def check_password(self): + """Check that the hash in self.pprime matches whats in the password safe. True if password matches hash in hpprime. False otherwise""" + hsh = sha256_func() + hsh.update(self.pprime) + return hsh.digest() == self.hpprime + + def update_pprime(self): + """Update self.pprime. This key is used to decrypt B1/2 and B3/4""" + self.pprime = stretchkey(self.password, self.salt, self.iter) + + def close(self): + """Close out open file""" + self.fl.close() + + def __del__(self): + try: + self.fl.close() + except: + pass + + def listall(self): + """Yield all entries in the form + (uuid, title, group, username, password, notes) + """ + def nrwrapper(name): + try: + return record[name] + except KeyError: + return None + + for record in self.records: + yield ( + nrwrapper('UUID') + , nrwrapper('Title') + , nrwrapper('Group') + , nrwrapper('Username') + , nrwrapper('Password') + , nrwrapper('Notes') + ) + + def getpass(self, uuid=None): + """Returns the password of the item with the given UUID""" + for record in self.records: + if record['UUID'] == uuid: + return record['Password'] + raise UUIDNotFoundError, "UUID %s was not found. " % repr(uuid) + + def __getitem__(self, *args, **kwargs): + return self.records.__getitem__(*args, **kwargs) + + def __setitem__(self, *args, **kwargs): + return self.records.__setitem__(*args, **kwargs) + + def getUUID(self): + """Return the safe's uuid""" + for hdr in self.headers: + if type(hdr) == UUIDHeader: + return hdr.uuid + return uuid4() + + def getVersion(self): + """Return the safe's version""" + for hdr in self.headers: + if type(hdr) == VersionHeader: + return hdr.version + +# Misc helper functions +def ispsafe3(filename): + """Return True if the file appears to be a psafe v3 file. Does not do in-depth checks. """ + fil = open(filename, "r") + data = fil.read(4) + fil.close() + return data == "PWS3" + +if __name__ == "__main__": + import doctest + doctest.testmod() + diff --git a/consts.py b/consts.py new file mode 100755 index 0000000..ba69bce --- /dev/null +++ b/consts.py @@ -0,0 +1,759 @@ +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== +''' PSafe constants +Created on Oct 27, 2010 + +@author: mcintyrep +''' + +# Configuration options +# Configuration Statics +ptApplication = 0 +ptDatabase = 1 +ptObsolete = 2 + +# Bools +conf_bools = { + 'AlwaysOnTop':{ + 'default':False, + 'type':ptApplication, + 'name':'AlwaysOnTop', + 'index':0 + }, + + 'ShowPWDefault':{ + 'default':False, + 'type':ptDatabase, + 'name':'ShowPWDefault', + 'index':1 + }, + + 'ShowPasswordInTree':{ + 'default':False, + 'type':ptDatabase, + 'name':'ShowPasswordInTree', + 'index':2 + }, + + 'SortAscending':{ + 'default':True, + 'type':ptDatabase, + 'name':'SortAscending', + 'index':3 + }, + + 'UseDefaultUser':{ + 'default':False, + 'type':ptDatabase, + 'name':'UseDefaultUser', + 'index':4 + }, + + 'SaveImmediately':{ + 'default':True, + 'type':ptDatabase, + 'name':'SaveImmediately', + 'index':5 + }, + + 'PWUseLowercase':{ + 'default':True, + 'type':ptDatabase, + 'name':'PWUseLowercase', + 'index':6 + }, + + 'PWUseUppercase':{ + 'default':True, + 'type':ptDatabase, + 'name':'PWUseUppercase', + 'index':7 + }, + + 'PWUseDigits':{ + 'default':True, + 'type':ptDatabase, + 'name':'PWUseDigits', + 'index':8 + }, + + 'PWUseSymbols':{ + 'default':False, + 'type':ptDatabase, + 'name':'PWUseSymbols', + 'index':9 + }, + + 'PWUseHexDigits':{ + 'default':False, + 'type':ptDatabase, + 'name':'PWUseHexDigits', + 'index':10 + }, + + 'PWUseEasyVision':{ + 'default':False, + 'type':ptDatabase, + 'name':'PWUseEasyVision', + 'index':11 + }, + + 'dontaskquestion':{ + 'default':False, + 'type':ptApplication, + 'name':'dontaskquestion', + 'index':12 + }, + + 'deletequestion':{ + 'default':False, + 'type':ptApplication, + 'name':'deletequestion', + 'index':13 + }, + + 'DCShowsPassword':{ + 'default':False, + 'type':ptApplication, + 'name':'DCShowsPassword', + 'index':14 + }, + + 'DontAskMinimizeClearYesNo':{ + 'default':True, + 'type':ptObsolete, + 'name':'DontAskMinimizeClearYesNo', + 'index':15 + }, + + 'DatabaseClear':{ + 'default':False, + 'type':ptApplication, + 'name':'DatabaseClear', + 'index':16 + }, + + 'DontAskSaveMinimize':{ + 'default':False, + 'type':ptObsolete, + 'name':'DontAskSaveMinimize', + 'index':17 + }, + + 'QuerySetDef':{ + 'default':True, + 'type':ptApplication, + 'name':'QuerySetDef', + 'index':18 + }, + + 'UseNewToolbar':{ + 'default':True, + 'type':ptApplication, + 'name':'UseNewToolbar', + 'index':19 + }, + + 'UseSystemTray':{ + 'default':True, + 'type':ptApplication, + 'name':'UseSystemTray', + 'index':20 + }, + + 'LockOnWindowLock':{ + 'default':True, + 'type':ptApplication, + 'name':'LockOnWindowLock', + 'index':21 + }, + + 'LockOnIdleTimeout':{ + 'default':True, + 'type':ptObsolete, + 'name':'LockOnIdleTimeout', + 'index':22 + }, + + 'EscExits':{ + 'default':True, + 'type':ptApplication, + 'name':'EscExits', + 'index':23 + }, + + 'IsUTF8':{ + 'default':False, + 'type':ptDatabase, + 'name':'IsUTF8', + 'index':24 + }, + + 'HotKeyEnabled':{ + 'default':False, + 'type':ptApplication, + 'name':'HotKeyEnabled', + 'index':25 + }, + + 'MRUOnFileMenu':{ + 'default':True, + 'type':ptApplication, + 'name':'MRUOnFileMenu', + 'index':26 + }, + + 'DisplayExpandedAddEditDlg':{ + 'default':True, + 'type':ptObsolete, + 'name':'DisplayExpandedAddEditDlg', + 'index':27 + }, + + 'MaintainDateTimeStamps':{ + 'default':False, + 'type':ptDatabase, + 'name':'MaintainDateTimeStamps', + 'index':28 + }, + + 'SavePasswordHistory':{ + 'default':False, + 'type':ptDatabase, + 'name':'SavePasswordHistory', + 'index':29 + }, + + 'FindWraps':{ + 'default':False, + 'type':ptObsolete, + 'name':'FindWraps', + 'index':30 + }, + + 'ShowNotesDefault':{ + 'default':False, + 'type':ptDatabase, + 'name':'ShowNotesDefault', + 'index':31 + }, + + 'BackupBeforeEverySave':{ + 'default':True, + 'type':ptApplication, + 'name':'BackupBeforeEverySave', + 'index':32 + }, + + 'PreExpiryWarn':{ + 'default':False, + 'type':ptApplication, + 'name':'PreExpiryWarn', + 'index':33 + }, + + 'ExplorerTypeTree':{ + 'default':False, + 'type':ptApplication, + 'name':'ExplorerTypeTree', + 'index':34 + }, + + 'ListViewGridLines':{ + 'default':False, + 'type':ptApplication, + 'name':'ListViewGridLines', + 'index':35 + }, + + 'MinimizeOnAutotype':{ + 'default':True, + 'type':ptApplication, + 'name':'MinimizeOnAutotype', + 'index':36 + }, + + 'ShowUsernameInTree':{ + 'default':True, + 'type':ptDatabase, + 'name':'ShowUsernameInTree', + 'index':37 + }, + + 'PWMakePronounceable':{ + 'default':False, + 'type':ptDatabase, + 'name':'PWMakePronounceable', + 'index':38 + }, + + 'ClearClipoardOnMinimize':{ + 'default':True, + 'type':ptObsolete, + 'name':'ClearClipoardOnMinimize', + 'index':39 + }, + + 'ClearClipoardOneExit':{ + 'default':True, + 'type':ptObsolete, + 'name':'ClearClipoardOneExit', + 'index':40 + }, + + 'ShowToolbar':{ + 'default':True, + 'type':ptApplication, + 'name':'ShowToolbar', + 'index':41 + }, + + 'ShowNotesAsToolTipsInViews':{ + 'default':False, + 'type':ptApplication, + 'name':'ShowNotesAsToolTipsInViews', + 'index':42 + }, + + 'DefaultOpenRO':{ + 'default':False, + 'type':ptApplication, + 'name':'DefaultOpenRO', + 'index':43 + }, + + 'MultipleInstances':{ + 'default':True, + 'type':ptApplication, + 'name':'MultipleInstances', + 'index':44 + }, + + 'ShowDragbar':{ + 'default':True, + 'type':ptApplication, + 'name':'ShowDragbar', + 'index':45 + }, + + 'ClearClipboardOnMinimize':{ + 'default':True, + 'type':ptApplication, + 'name':'ClearClipboardOnMinimize', + 'index':46 + }, + + 'ClearClipboardOnExit':{ + 'default':True, + 'type':ptApplication, + 'name':'ClearClipboardOnExit', + 'index':47 + }, + + 'ShowFindToolBarOnOpen':{ + 'default':False, + 'type':ptApplication, + 'name':'ShowFindToolBarOnOpen', + 'index':48 + }, + + 'NotesWordWrap':{ + 'default':False, + 'type':ptApplication, + 'name':'NotesWordWrap', + 'index':49 + }, + + 'LockDBOnIdleTimeout':{ + 'default':True, + 'type':ptDatabase, + 'name':'LockDBOnIdleTimeout', + 'index':50 + }, + + 'HighlightChanges':{ + 'default':True, + 'type':ptApplication, + 'name':'HighlightChanges', + 'index':51 + }, + + 'HideSystemTray':{ + 'default':False, + 'type':ptApplication, + 'name':'HideSystemTray', + 'index':52 + }, +} + +# Ints +conf_ints = { + 'column1width':{ + 'name':'column1width', + 'default':65535, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':0, + }, + + 'column2width':{ + 'name':'column2width', + 'default':65535, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':1, + }, + + 'column3width':{ + 'name':'column3width', + 'default':65535, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':2, + }, + + 'column4width':{ + 'name':'column4width', + 'default':65535, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':3, + }, + + 'sortedcolumn':{ + 'name':'sortedcolumn', + 'default':0, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':4, + }, + + 'PWDefaultLength':{ + 'name':'PWDefaultLength', + 'default':8, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':5, + }, + + 'maxmruitems':{ + 'name':'maxmruitems', + 'default':4, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':6, + }, + + 'IdleTimeout':{ + 'name':'IdleTimeout', + 'default':5, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':7, + }, + + 'DoubleClickAction':{ + 'name':'DoubleClickAction', + 'default':0, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':8, + }, + + 'HotKey':{ + 'name':'HotKey', + 'default':0, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':9, + }, + + 'MaxREItems':{ + 'name':'MaxREItems', + 'default':25, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':10, + }, + + 'TreeDisplayStatusAtOpen':{ + 'name':'TreeDisplayStatusAtOpen', + 'default':0, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':11, + }, + + 'NumPWHistoryDefault':{ + 'name':'NumPWHistoryDefault', + 'default':3, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':12, + }, + + 'BackupSuffix':{ + 'name':'BackupSuffix', + 'default':0, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':13, + }, + + 'BackupMaxIncremented':{ + 'name':'BackupMaxIncremented', + 'default':1, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':14, + }, + + 'PreExpiryWarnDays':{ + 'name':'PreExpiryWarnDays', + 'default':1, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':15, + }, + + 'ClosedTrayIconColour':{ + 'name':'ClosedTrayIconColour', + 'default':0, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':16, + }, + + 'PWDigitMinLength':{ + 'name':'PWDigitMinLength', + 'default':0, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':17, + }, + + 'PWLowercaseMinLength':{ + 'name':'PWLowercaseMinLength', + 'default':0, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':18, + }, + + 'PWSymbolMinLength':{ + 'name':'PWSymbolMinLength', + 'default':0, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':19, + }, + + 'PWUppercaseMinLength':{ + 'name':'PWUppercaseMinLength', + 'default':0, + 'type':ptDatabase, + 'min':-1, + 'max':-1, + 'index':20, + }, + + 'OptShortcutColumnWidth':{ + 'name':'OptShortcutColumnWidth', + 'default':92, + 'type':ptApplication, + 'min':-1, + 'max':-1, + 'index':21, + }, +} + +# Strings +conf_strs = { + 'currentbackup':{ + 'name':'currentbackup', + 'default':'', + 'type':ptApplication, + 'index':0, + }, + + 'currentfile':{ + 'name':'currentfile', + 'default':'', + 'type':ptApplication, + 'index':1, + }, + + 'lastview':{ + 'name':'lastview', + 'default':'tree', + 'type':ptApplication, + 'index':2, + }, + + 'DefaultUsername':{ + 'name':'DefaultUsername', + 'default':'', + 'type':ptDatabase, + 'index':3, + }, + + 'treefont':{ + 'name':'treefont', + 'default':'', + 'type':ptApplication, + 'index':4, + }, + + 'BackupPrefixValue':{ + 'name':'BackupPrefixValue', + 'default':'', + 'type':ptApplication, + 'index':5, + }, + + 'BackupDir':{ + 'name':'BackupDir', + 'default':'', + 'type':ptApplication, + 'index':6, + }, + + 'AltBrowser':{ + 'name':'AltBrowser', + 'default':'', + 'type':ptApplication, + 'index':7, + }, + + 'ListColumns':{ + 'name':'ListColumns', + 'default':'', + 'type':ptApplication, + 'index':8, + }, + + 'ColumnWidths':{ + 'name':'ColumnWidths', + 'default':'', + 'type':ptApplication, + 'index':9, + }, + + 'DefaultAutotypeString':{ + 'name':'DefaultAutotypeString', + 'default':'', + 'type':ptDatabase, + 'index':10, + }, + + 'AltBrowserCmdLineParms':{ + 'name':'AltBrowserCmdLineParms', + 'default':'', + 'type':ptApplication, + 'index':11, + }, + + 'MainToolBarButtons':{ + 'name':'MainToolBarButtons', + 'default':'', + 'type':ptApplication, + 'index':12, + }, + + 'PasswordFont':{ + 'name':'PasswordFont', + 'default':'', + 'type':ptApplication, + 'index':13, + }, + + 'TreeListSampleText':{ + 'name':'TreeListSampleText', + 'default':'AaBbYyZz 0O1IlL', + 'type':ptApplication, + 'index':14, + }, + + 'PswdSampleText':{ + 'name':'PswdSampleText', + 'default':'AaBbYyZz 0O1IlL', + 'type':ptApplication, + 'index':15, + }, + + 'LastUsedKeyboard':{ + 'name':'LastUsedKeyboard', + 'default':'', + 'type':ptApplication, + 'index':16, + }, + + 'VKeyboardFontName':{ + 'name':'VKeyboardFontName', + 'default':'', + 'type':ptApplication, + 'index':17, + }, + + 'VKSampleText':{ + 'name':'VKSampleText', + 'default':'AaBbYyZz 0O1IlL', + 'type':ptApplication, + 'index':18, + }, + + 'AltNotesEditor':{ + 'name':'AltNotesEditor', + 'default':'', + 'type':ptApplication, + 'index':19, + }, +} + +# Type Mappings +conf_types = {} +for name, info in conf_bools.items(): + conf_types[name] = bool +for name, info in conf_ints.items(): + conf_types[name] = int +for name, info in conf_strs.items(): + conf_types[name] = str + + + diff --git a/errors.py b/errors.py new file mode 100755 index 0000000..8f73577 --- /dev/null +++ b/errors.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +#=============================================================================== +# SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. +# +# This file is part of PyPWSafe. +# +# PyPWSafe 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. +# +# PyPWSafe 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 PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +#=============================================================================== +""" + + +""" +class PSafeError(StandardError): + """Base passsafe error""" + +class PasswordError(PSafeError): + """Password does not match the password safe""" + +class InvalidHMACError(PSafeError): + """Calculated HMAC does not equal HMAC in the file""" + +class ROSafe(PSafeError): + """ Safe is not in read/write mode """ + +class UUIDNotFoundError(PSafeError): + """UUID was not found""" + +class RecordError(PSafeError): + """Failed to perform an action in a record""" + +class AccessError(PSafeError): + """Insufficient permissions to access a psafe file""" + +class ROSafeError(PSafeError): + """A write request was made on a read-only safe""" + +class PropError(RecordError): + """Failed to perform an action with a property""" + +class PropParsingError(PropError): + """Failed to parse a property""" + +class HeaderError(PSafeError): + """ Error in the headers """ + +class PrefrencesHeaderError(HeaderError): + """ An error occurred in the preferences header """ + +class PrefsValueError(PrefrencesHeaderError): + """ Unexpected or improper value for the header preference """ + +class PrefsDataTypeError(PrefrencesHeaderError): + """ Error parsing the preferences type of the preferences + header record""" + +class ConfigItemNotFoundError(PrefrencesHeaderError): + """ No such preference """ + +class UnableToFindADelimitersError(PrefrencesHeaderError): + """ Couldn't find an unused char to delminate the string""" +