def move_group_in_parent(self, group=None, index=None): """Move group to another position in group's parent. index must be a valid index of group.parent.groups """ if group is None or index is None: raise KPError("group and index must be set") elif type(group) is not v1Group or type(index) is not int: raise KPError("group must be a v1Group-instance and index " "must be an integer.") elif group not in self.groups: raise KPError("Given group doesn't exist") elif index < 0 or index >= len(group.parent.children): raise KPError("index must be a valid index if group.parent.groups") else: group_at_index = group.parent.children[index] pos_in_parent = group.parent.children.index(group) pos_in_groups = self.groups.index(group) pos_in_groups2 = self.groups.index(group_at_index) group.parent.children[index] = group group.parent.children[pos_in_parent] = group_at_index self.groups[pos_in_groups2] = group self.groups[pos_in_groups] = group_at_index if group.children: self._move_group_helper(group) if group_at_index.children: self._move_group_helper(group_at_index) group.last_mod = datetime.now().replace(microsecond=0) return True
def move_entry_in_group(self, entry=None, index=None): """Move entry to another position inside a group. An entry and a valid index to insert the entry in the entry list of the holding group is needed. 0 means that the entry is moved to the first position 1 to the second and so on. """ if entry is None or index is None or type(entry) is not v1Entry \ or type(index) is not int: raise KPError("Need an entry and an index.") elif index < 0 or index > len(entry.group.entries) - 1: raise KPError("Index is not valid.") elif entry not in self.entries: raise KPError("Entry not found.") pos_in_group = entry.group.entries.index(entry) pos_in_entries = self.entries.index(entry) entry_at_index = entry.group.entries[index] pos_in_entries2 = self.entries.index(entry_at_index) entry.group.entries[index] = entry entry.group.entries[pos_in_group] = entry_at_index self.entries[pos_in_entries2] = entry self.entries[pos_in_entries] = entry_at_index return True
def create_entry(self, group = None, title = "", image = 1, url = "", username = "", password = "", comment = "", y = 2999, mon = 12, d = 28, h = 23, min_ = 59, s = 59): """This method creates a new entry. The group which should hold the entry is needed. image must be an unsigned int >0, group a v1Group. It is possible to give an expire date in the following way: - y is the year between 1 and 9999 inclusive - mon is the month between 1 and 12 - d is a day in the given month - h is a hour between 0 and 23 - min_ is a minute between 0 and 59 - s is a second between 0 and 59 The special date 2999-12-28 23:59:59 means that entry expires never. """ if (type(title) is not str or type(image) is not int or image < 0 or type(url) is not str or type(username) is not str or type(password) is not str or type(comment) is not str or type(y) is not int or type(mon) is not int or type(d) is not int or type(h) is not int or type(min_) is not int or type(s) is not int or type(group) is not v1Group): raise KPError("One argument has not a valid type.") elif group not in self.groups: raise KPError("Group doesn't exist.") elif (y > 9999 or y < 1 or mon > 12 or mon < 1 or d > 31 or d < 1 or h > 23 or h < 0 or min_ > 59 or min_ < 0 or s > 59 or s < 0): raise KPError("No legal date") elif (((mon == 1 or mon == 3 or mon == 5 or mon == 7 or mon == 8 or mon == 10 or mon == 12) and d > 31) or ((mon == 4 or mon == 6 or mon == 9 or mon == 11) and d > 30) or (mon == 2 and d > 28)): raise KPError("Given day doesn't exist in given month") Random.atfork() uuid = Random.get_random_bytes(16) entry = v1Entry(group.id_, group, image, title, url, username, password, comment, datetime.now().replace(microsecond = 0), datetime.now().replace(microsecond = 0), datetime.now().replace(microsecond = 0), datetime(y, mon, d, h, min_, s), uuid) self.entries.append(entry) group.entries.append(entry) self._num_entries += 1 return True
def _get_filekey(self): """This method creates a key from a keyfile.""" if not os.path.exists(self.keyfile): raise KPError('Keyfile not exists.') try: with open(self.keyfile, 'rb') as handler: handler.seek(0, os.SEEK_END) size = handler.tell() handler.seek(0, os.SEEK_SET) if size == 32: return handler.read(32) elif size == 64: try: return binascii.unhexlify(handler.read(64)) except (TypeError, binascii.Error): handler.seek(0, os.SEEK_SET) sha = SHA256.new() while True: buf = handler.read(2048) sha.update(buf) if len(buf) < 2048: break return sha.digest() except IOError as e: raise KPError('Could not read file: %s' % e)
def set_expire(self, y = 2999, mon = 12, d = 28, h = 23, min_ = 59, s = 59): """This method is used to change the expire date of an entry. - y is the year between 1 and 9999 inclusive - mon is the month between 1 and 12 - d is a day in the given month - h is a hour between 0 and 23 - min_ is a minute between 0 and 59 - s is a second between 0 and 59 The special date 2999-12-28 23:59:59 means that expires never. If only an uuid is given the expire date will set to this one. """ if type(y) is not int or type(mon) is not int or type(d) is not int or \ type(h) is not int or type(min_) is not int or type(s) is not int: raise KPError("Date variables must be integers") elif y > 9999 or y < 1 or mon > 12 or mon < 1 or d > 31 or d < 1 or \ h > 23 or h < 0 or min_ > 59 or min_ < 0 or s > 59 or s < 0: raise KPError("No legal date") elif ((mon == 1 or mon == 3 or mon == 5 or mon == 7 or mon == 8 or \ mon == 10 or mon == 12) and d > 31) or ((mon == 4 or mon == 6 or \ mon == 9 or mon == 11) and d > 30) or (mon == 2 and d > 28): raise KPError("Given day doesn't exist in given month") else: self.expire = datetime(y, mon, d, h, min_, s) self.last_mod = datetime.now().replace(microsecond = 0) return True
def _create_group_tree(self, levels): """This method creates a group tree""" if levels[0] != 0: raise KPError("Invalid group tree") for i in range(len(self.groups)): if (levels[i] == 0): self.groups[i].parent = self.root_group self.groups[i].index = len(self.root_group.children) self.root_group.children.append(self.groups[i]) continue j = i - 1 while j >= 0: if levels[j] < levels[i]: if levels[i] - levels[j] != 1: raise KPError("Invalid group tree") self.groups[i].parent = self.groups[j] self.groups[i].index = len(self.groups[j].children) self.groups[i].parent.children.append(self.groups[i]) break if j == 0: raise KPError("Invalid group tree") j -= 1 for e in range(len(self.entries)): for g in range(len(self.groups)): if self.entries[e].group_id == self.groups[g].id_: self.groups[g].entries.append(self.entries[e]) self.entries[e].group = self.groups[g] # from original KeePassX-code, but what does it do? self.entries[e].index = 0 return True
def remove_group(self, group=None): """This method removes a group. The group needed to remove the group. group must be a v1Group. """ if group is None: raise KPError("Need group to remove a group") elif type(group) is not v1Group: raise KPError("group must be v1Group") children = [] entries = [] if group in self.groups: # Save all children and entries to # delete them later children.extend(group.children) entries.extend(group.entries) # Finally remove group group.parent.children.remove(group) self.groups.remove(group) else: raise KPError("Given group doesn't exist") self._num_groups -= 1 for i in children: self.remove_group(i) for i in entries: self.remove_entry(i) return True
def __init__(self, filepath=None, password=None, keyfile=None, read_only=False, new = False): """ Initialize a new or an existing database. If a 'filepath' and a 'masterkey' is passed 'load' will try to open a database. If 'True' is passed to 'read_only' the database will open read-only. It's also possible to create a new one, just pass 'True' to new. This will be ignored if a filepath and a masterkey is given this will be ignored. """ if filepath is not None and password is None and keyfile is None: raise KPError('Missing argument: Password or keyfile ' 'needed additionally to open an existing database!') elif type(read_only) is not bool or type(new) is not bool: raise KPError('read_only and new must be bool') elif ((filepath is not None and type(filepath) is not str) or (type(password) is not str and password is not None) or (type(keyfile) is not str and keyfile is not None)): raise KPError('filepath, masterkey and keyfile must be a string') elif (filepath is None and password is None and keyfile is None and new is False): raise KPError('Either an existing database should be opened or ' 'a new should be created.') self.groups = [] self.entries = [] self.root_group = v1Group() self.read_only = read_only self.filepath = filepath self.password = password self.keyfile = keyfile # This are attributes that are needed internally. You should not # change them directly, it could damage the database! self._group_order = [] self._entry_order = [] self._signature1 = 0x9AA2D903 self._signature2 = 0xB54BFB65 self._enc_flag = 2 self._version = 0x00030002 self._final_randomseed = '' self._enc_iv = '' self._num_groups = 1 self._num_entries = 0 self._contents_hash = '' Random.atfork() self._transf_randomseed = Random.get_random_bytes(32) self._key_transf_rounds = 150000 # Due to the design of KeePass, at least one group is needed. if new is True: self._group_order = [("id", 1), (1, 4), (2, 9), (7, 4), (8, 2), (0xFFFF, 0)] group = v1Group(1, 'Internet', 1, self, parent = self.root_group) self.root_group.children.append(group) self.groups.append(group)
def create_group(self, title=None, parent=None, image=1, y=2999, mon=12, d=28, h=23, min_=59, s=59): """This method creates a new group. A group title is needed or no group will be created. If a parent is given, the group will be created as a sub-group. title must be a string, image an unsigned int >0 and parent a v1Group. With y, mon, d, h, min_ and s you can set an expiration date like on entries. """ if title is None: raise KPError("Need a group title to create a group.") elif type(title) is not str or image < 1 or(parent is not None and \ type(parent) is not v1Group) or type(image) is not int: raise KPError("Wrong type or value for title or image or parent") id_ = 1 for i in self.groups: if i.id_ >= id_: id_ = i.id_ + 1 group = v1Group(id_, title, image, self) group.creation = datetime.now().replace(microsecond=0) group.last_mod = datetime.now().replace(microsecond=0) group.last_access = datetime.now().replace(microsecond=0) if group.set_expire(y, mon, d, h, min_, s) is False: group.set_expire() # If no parent is given, just append the new group at the end if parent is None: group.parent = self.root_group self.root_group.children.append(group) group.level = 0 self.groups.append(group) # Else insert the group behind the parent else: if parent in self.groups: parent.children.append(group) group.parent = parent group.level = parent.level + 1 self.groups.insert(self.groups.index(parent) + 1, group) else: raise KPError("Given parent doesn't exist") self._num_groups += 1 return True
def remove_entry(self, entry=None): """This method can remove entries. The v1Entry-object entry is needed. """ if entry is None or type(entry) is not v1Entry: raise KPError("Need an entry.") elif entry in self.entries: entry.group.entries.remove(entry) self.entries.remove(entry) self._num_entries -= 1 return True else: raise KPError("Given entry doesn't exist.")
def move_entry(self, entry=None, group=None): """Move an entry to another group. A v1Group group and a v1Entry entry are needed. """ if entry is None or group is None or type(entry) is not v1Entry or \ type(group) is not v1Group: raise KPError("Need an entry and a group.") elif entry not in self.entries: raise KPError("No entry found.") elif group in self.groups: entry.group.entries.remove(entry) group.entries.append(entry) entry.group_id = group.id_ entry.group = group return True else: raise KPError("No group found.")
def unlock(self, password=None, keyfile=None, buf=None): """Unlock the database. masterkey is needed. """ if ((password is None or password == "") and (keyfile is None or keyfile == "")): raise KPError("A password/keyfile is needed") elif ((type(password) is not str and password is not None) or (type(keyfile) is not str and keyfile is not None)): raise KPError("password/keyfile must be a string.") if keyfile == "": keyfile = None if password == "": password = None self.password = password self.keyfile = keyfile return self.load(buf)
def close(self): """This method closes the database correctly.""" if self.filepath is not None: if path.isfile(self.filepath + '.lock'): remove(self.filepath + '.lock') self.filepath = None self.read_only = False self.lock() return True else: raise KPError('Can\'t close a not opened file')
def set_username(self, username = None): """This method is used to set the username. username must be a string. """ if username is None or type(username) is not str: raise KPError("Need a new image number") else: self.username = username self.last_mod = datetime.now().replace(microsecond=0) return True
def set_url(self, url = None): """This method is used to set the url. url must be a string. """ if url is None or type(url) is not str: raise KPError("Need a new image number") else: self.url = url self.last_mod = datetime.now().replace(microsecond=0) return True
def set_image(self, image = None): """This method is used to set the image number. image must be an unsigned int. """ if image is None or type(image) is not int: raise KPError("Need a new image number") else: self.image = image self.last_mod = datetime.now().replace(microsecond=0) return True
def set_title(self, title = None): """This method is used to change an entry title. A new title string is needed. """ if title is None or type(title) is not str: raise KPError("Need a new title.") else: self.title = title self.last_mod = datetime.now().replace(microsecond=0) return True
def set_comment(self, comment = None): """This method is used to the the comment. comment must be a string. """ if comment is None or type(comment) is not str: raise KPError("Need a new image number") else: self.comment = comment self.last_mod = datetime.now().replace(microsecond=0) return True
def set_password(self, password = None): """This method is used to set the password. password must be a string. """ if password is None or type(password) is not str: raise KPError("Need a new image number") else: self.password = password self.last_mod = datetime.now().replace(microsecond=0) return True
def set_image(self, image=None): """This method is used to change the image number of a group. image must be an unsigned int >0. """ if image is None or type(image) is not int or image < 1: raise KPError("Need a group and an image number!") else: self.image = image self.last_mod = datetime.now().replace(microsecond=0) return True
def move_group(self, group=None, parent=None): """Append group to a new parent. group and parent must be v1Group-instances. """ if group is None or type(group) is not v1Group: raise KPError("A valid group must be given.") elif parent is not None and type(parent) is not v1Group: raise KPError("parent must be a v1Group.") elif group is parent: raise KPError("group and parent must not be the same group") if parent is None: parent = self.root_group if group in self.groups: self.groups.remove(group) group.parent.children.remove(group) group.parent = parent if parent.children: if parent.children[-1] is self.groups[-1]: self.groups.append(group) else: new_index = self.groups.index(parent.children[-1]) + 1 self.groups.insert(new_index, group) else: new_index = self.groups.index(parent) + 1 self.groups.insert(new_index, group) parent.children.append(group) if parent is self.root_group: group.level = 0 else: group.level = parent.level + 1 if group.children: self._move_group_helper(group) group.last_mod = datetime.now().replace(microsecond=0) return True else: raise KPError("Didn't find given group.")
def read_buf(self): """Read database file""" with open(self.filepath, 'rb') as handler: try: buf = handler.read() # There should be a header at least if len(buf) < 124: raise KPError('Unexpected file size. It should be more or' 'equal 124 bytes but it is {0}!'.format( len(buf))) except: raise return buf
def load(self, buf=None): """This method opens an existing database. self.password/self.keyfile and self.filepath must be set. """ if self.password is None and self.keyfile is None: raise KPError('Need a password or keyfile') elif self.filepath is None and buf is None: raise KPError('Can only load an existing database!') if buf is None: buf = self.read_buf() # The header is 124 bytes long, the rest is content header = buf[:124] crypted_content = buf[124:] del buf # The header holds two signatures if not (struct.unpack('<I', header[:4])[0] == 0x9AA2D903 and struct.unpack('<I', header[4:8])[0] == 0xB54BFB65): del crypted_content del header raise KPError('Wrong signatures!') # Unpack the header self._enc_flag = struct.unpack('<I', header[8:12])[0] self._version = struct.unpack('<I', header[12:16])[0] self._final_randomseed = struct.unpack('<16s', header[16:32])[0] self._enc_iv = struct.unpack('<16s', header[32:48])[0] self._num_groups = struct.unpack('<I', header[48:52])[0] self._num_entries = struct.unpack('<I', header[52:56])[0] self._contents_hash = struct.unpack('<32s', header[56:88])[0] self._transf_randomseed = struct.unpack('<32s', header[88:120])[0] self._key_transf_rounds = struct.unpack('<I', header[120:124])[0] del header # Check if the database is supported if self._version & 0xFFFFFF00 != 0x00030002 & 0xFFFFFF00: del crypted_content raise KPError('Unsupported file version!') #Actually, only AES is supported. elif not self._enc_flag & 2: del crypted_content raise KPError('Unsupported file encryption!') if self.password is None: masterkey = self._get_filekey() elif self.password is not None and self.keyfile is not None: passwordkey = self._get_passwordkey() filekey = self._get_filekey() sha = SHA256.new() sha.update(passwordkey + filekey) masterkey = sha.digest() else: masterkey = self._get_passwordkey() # Create the key that is needed to... final_key = self._transform_key(masterkey) # ...decrypt the content decrypted_content = self._cbc_decrypt(final_key, crypted_content) # Check if decryption failed if ((len(decrypted_content) > 2147483446) or (len(decrypted_content) == 0 and self._num_groups > 0)): del decrypted_content del crypted_content raise KPError("Decryption failed!\nThe key is wrong or the file is" " damaged.") sha_obj = SHA256.new() sha_obj.update(decrypted_content) if not self._contents_hash == sha_obj.digest(): del masterkey del final_key raise KPError("Hash test failed.\nThe key is wrong or the file is " "damaged.") del masterkey del final_key # Read out the groups pos = 0 levels = [] cur_group = 0 group = v1Group() while cur_group < self._num_groups: # Every group is made up of single fields field_type = struct.unpack('<H', decrypted_content[:2])[0] decrypted_content = decrypted_content[2:] pos += 2 # Check if offset is alright if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G1]') field_size = struct.unpack('<I', decrypted_content[:4])[0] decrypted_content = decrypted_content[4:] pos += 4 if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G2]') # Finally read out the content b_ret = self._read_group_field(group, levels, field_type, field_size, decrypted_content) # If the end of a group is reached append it to the groups array if field_type == 0xFFFF and b_ret == True: group.db = self self.groups.append(group) group = v1Group() cur_group += 1 decrypted_content = decrypted_content[field_size:] if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G1]') # Now the same with the entries cur_entry = 0 entry = v1Entry() while cur_entry < self._num_entries: field_type = struct.unpack('<H', decrypted_content[:2])[0] decrypted_content = decrypted_content[2:] pos += 2 if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G1]') field_size = struct.unpack('<I', decrypted_content[:4])[0] decrypted_content = decrypted_content[4:] pos += 4 if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G2]') b_ret = self._read_entry_field(entry, field_type, field_size, decrypted_content) if field_type == 0xFFFF and b_ret == True: self.entries.append(entry) if entry.group_id is None: del decrypted_content del crypted_content raise KPError("Found entry without group!") entry = v1Entry() cur_entry += 1 decrypted_content = decrypted_content[field_size:] pos += field_size if pos >= len(crypted_content) + 124: del decrypted_content del crypted_content raise KPError('Unexpected error: Offset is out of range.[G1]') if self._create_group_tree(levels) is False: del decrypted_content del crypted_content return False del decrypted_content del crypted_content if self.filepath is not None: with open(self.filepath + '.lock', 'w') as handler: handler.write('') return True
def save(self, filepath=None, password=None, keyfile=None): """This method saves the database. It's possible to parse a data path to an alternative file. """ if (password is None and keyfile is not None and keyfile != "" and type(keyfile) is str): self.keyfile = keyfile elif (keyfile is None and password is not None and password != "" and type(password is str)): self.password = password elif (keyfile is not None and password is not None and keyfile != "" and password != "" and type(keyfile) is str and type(password) is str): self.keyfile = keyfile self.password = password if self.read_only: raise KPError("The database has been opened read-only.") elif ((self.password is None and self.keyfile is None) or (filepath is None and self.filepath is None) or (keyfile == "" and password == "")): raise KPError("Need a password/keyfile and a filepath to save the " "file.") elif ((type(self.filepath) is not str and self.filepath is not None) or (type(self.password) is not str and self.password is not None) or (type(self.keyfile) is not str and self.keyfile is not None)): raise KPError("filepath, password and keyfile must be strings.") elif self._num_groups == 0: raise KPError("Need at least one group!") content = bytearray() # First, read out all groups for i in self.groups: # Get the packed bytes # j stands for a possible field type for j in range(1, 10): ret_save = self._save_group_field(j, i) # The field type and the size is always in front of the data if ret_save is not False: content += struct.pack('<H', j) content += struct.pack('<I', ret_save[0]) content += ret_save[1] # End of field content += struct.pack('<H', 0xFFFF) content += struct.pack('<I', 0) # Same with entries for i in self.entries: for j in range(1, 15): ret_save = self._save_entry_field(j, i) if ret_save is not False: content += struct.pack('<H', j) content += struct.pack('<I', ret_save[0]) content += ret_save[1] content += struct.pack('<H', 0xFFFF) content += struct.pack('<I', 0) # Generate new seed and new vector; calculate the new hash Random.atfork() self._final_randomseed = Random.get_random_bytes(16) self._enc_iv = Random.get_random_bytes(16) sha_obj = SHA256.new() sha_obj.update(bytes(content)) self._contents_hash = sha_obj.digest() del sha_obj # Pack the header header = bytearray() header += struct.pack('<I', 0x9AA2D903) header += struct.pack('<I', 0xB54BFB65) header += struct.pack('<I', self._enc_flag) header += struct.pack('<I', self._version) header += struct.pack('<16s', self._final_randomseed) header += struct.pack('<16s', self._enc_iv) header += struct.pack('<I', self._num_groups) header += struct.pack('<I', self._num_entries) header += struct.pack('<32s', self._contents_hash) header += struct.pack('<32s', self._transf_randomseed) if self._key_transf_rounds < 150000: self._key_tranf_rounds = 150000 header += struct.pack('<I', self._key_transf_rounds) # Finally encrypt everything... if self.password is None: masterkey = self._get_filekey() elif self.password is not None and self.keyfile is not None: passwordkey = self._get_passwordkey() filekey = self._get_filekey() sha = SHA256.new() sha.update(passwordkey + filekey) masterkey = sha.digest() else: masterkey = self._get_passwordkey() final_key = self._transform_key(masterkey) encrypted_content = self._cbc_encrypt(content, final_key) del content del masterkey del final_key # ...and write it out if filepath is not None: try: handler = open(filepath, "wb") except IOError: raise KPError("Can't open {0}".format(filepath)) if self.filepath is None: self.filepath = filepath elif filepath is None and self.filepath is not None: try: handler = open(self.filepath, "wb") except IOError: raise KPError("Can't open {0}".format(self.filepath)) else: raise KPError("Need a filepath.") try: handler.write(header + encrypted_content) except IOError: raise KPError("Can't write to file.") finally: handler.close() if not path.isfile(self.filepath + ".lock"): try: lock = open(self.filepath + ".lock", "w") lock.write('') except IOError: raise KPError( "Can't create lock-file {0}".format(self.filepath + ".lock")) else: lock.close() return True