def save(self, dbfile=None, password=None, keyfile=None): """ Save the database to specified file/stream with password and/or keyfile. :param dbfile: The path to the file we wish to save. :type dbfile: The path to the database file or a file-like object. :param password: The password to use for the database encryption key. :type password: str :param keyfile: The path to keyfile (or a stream) to use instead of or in conjunction with password for encryption key. :type keyfile: str or file-like object :raise keepassdb.exc.ReadOnlyDatabase: If database was opened with readonly flag. """ if self.readonly: # We might wish to make this more sophisticated. E.g. if a new path is specified # as a parameter, then it's probably ok to ignore a readonly flag? In general # this flag doens't make a ton of sense for a library ... raise exc.ReadOnlyDatabase() if dbfile is not None and not hasattr(dbfile, 'write'): self.filepath = dbfile if password is not None or self.keyfile is not None: # Do these together so we don't end up with some hybrid of old & new key material self.password = password self.keyfile = keyfile else: raise ValueError("Password and/or keyfile is required.") if self.filepath is None and dbfile is None: raise ValueError("Unable to save without target file.") buf = bytearray() # First, serialize the groups for group in self.groups: # Get the packed bytes group_struct = group.to_struct() self.log.debug("Group struct: {0!r}".format(group_struct)) buf += group_struct.encode() # Then the entries. for entry in self.entries: entry_struct = entry.to_struct() buf += entry_struct.encode() # Hmmmm ... these defaults should probably be set elsewhere....? header = HeaderStruct() header.signature1 = const.DB_SIGNATURE1 header.signature2 = const.DB_SIGNATURE2 header.flags = header.AES header.version = 0x00030002 header.key_enc_rounds = 50000 header.seed_key = get_random_bytes(32) # Convert buffer to bytes for API simplicity buf = bytes(buf) # Generate new seed & vector; update content hash header.encryption_iv = get_random_bytes(16) header.seed_rand = get_random_bytes(16) header.contents_hash = hashlib.sha256(buf).digest() self.log.debug("(Unencrypted) content: {0!r}".format(buf)) self.log.debug("Generating hash for {0}-byte content: {1}".format(len(buf), hashlib.sha256(buf).digest())) # Update num groups/entries to match curr state header.nentries = len(self.entries) header.ngroups = len(self.groups) final_key = util.derive_key(seed_key=header.seed_key, seed_rand=header.seed_rand, rounds=header.key_enc_rounds, password=password, keyfile=keyfile) # FIXME: Remove this once we've tracked down issues. self.log.debug("(save) Final key: {0!r}, pass={1}".format(final_key, password)) encrypted_content = util.encrypt_aes_cbc(buf, key=final_key, iv=header.encryption_iv) if hasattr(dbfile, 'write'): dbfile.write(header.encode() + encrypted_content) else: with open(self.filepath, "wb") as fp: fp.write(header.encode() + encrypted_content)
def save(self, dbfile=None, password=None, keyfile=None): """ Save the database to specified file/stream with password and/or keyfile. :param dbfile: The path to the file we wish to save. :type dbfile: The path to the database file or a file-like object. :param password: The password to use for the database encryption key. :type password: str :param keyfile: The path to keyfile (or a stream) to use instead of or in conjunction with password for encryption key. :type keyfile: str or file-like object :raise keepassdb.exc.ReadOnlyDatabase: If database was opened with readonly flag. """ if self.readonly: # We might wish to make this more sophisticated. E.g. if a new path is specified # as a parameter, then it's probably ok to ignore a readonly flag? In general # this flag doens't make a ton of sense for a library ... raise exc.ReadOnlyDatabase() if dbfile is not None and not hasattr(dbfile, 'write'): self.filepath = dbfile if password is not None or self.keyfile is not None: # Do these together so we don't end up with some hybrid of old & new key material self.password = password self.keyfile = keyfile else: raise ValueError("Password and/or keyfile is required.") if self.filepath is None and dbfile is None: raise ValueError("Unable to save without target file.") buf = bytearray() # First, serialize the groups for group in self.groups: # Get the packed bytes group_struct = group.to_struct() self.log.debug("Group struct: {0!r}".format(group_struct)) buf += group_struct.encode() # Then the entries. for entry in self.entries: entry_struct = entry.to_struct() buf += entry_struct.encode() # Hmmmm ... these defaults should probably be set elsewhere....? header = HeaderStruct() header.signature1 = const.DB_SIGNATURE1 header.signature2 = const.DB_SIGNATURE2 header.flags = header.AES header.version = 0x00030002 header.key_enc_rounds = 50000 header.seed_key = get_random_bytes(32) # Convert buffer to bytes for API simplicity buf = bytes(buf) # Generate new seed & vector; update content hash header.encryption_iv = get_random_bytes(16) header.seed_rand = get_random_bytes(16) header.contents_hash = hashlib.sha256(buf).digest() self.log.debug("(Unencrypted) content: {0!r}".format(buf)) self.log.debug("Generating hash for {0}-byte content: {1}".format( len(buf), hashlib.sha256(buf).digest())) # Update num groups/entries to match curr state header.nentries = len(self.entries) header.ngroups = len(self.groups) final_key = util.derive_key(seed_key=header.seed_key, seed_rand=header.seed_rand, rounds=header.key_enc_rounds, password=password, keyfile=keyfile) # FIXME: Remove this once we've tracked down issues. self.log.debug("(save) Final key: {0!r}, pass={1}".format( final_key, password)) encrypted_content = util.encrypt_aes_cbc(buf, key=final_key, iv=header.encryption_iv) if hasattr(dbfile, 'write'): dbfile.write(header.encode() + encrypted_content) else: with open(self.filepath, "wb") as fp: fp.write(header.encode() + encrypted_content)
def load_from_buffer(self, buf, password=None, keyfile=None, readonly=False): """ Load a database from passed-in buffer (bytes). :param buf: A string (bytes) of the database contents. :type buf: str :param password: The password for the database. :type password: str :param keyfile: Path to a keyfile (or a stream) that can be used instead of or in conjunction with password for database. :type keyfile: str or file-like object :param readonly: Whether to open the database read-only. :type readonly: bool """ if password is None and keyfile is None: raise ValueError("Password and/or keyfile is required.") # Save these to use as defaults when saving the database self.password = password self.keyfile = keyfile # The header is 124 bytes long, the rest is content hdr_len = HeaderStruct.length header_bytes = buf[:hdr_len] crypted_content = buf[hdr_len:] self.header = HeaderStruct(header_bytes) self.log.debug("Extracted header: {0}".format(self.header)) # Check if the database is supported if self.header.version & const.DB_SUPPORTED_VERSION_MASK != const.DB_SUPPORTED_VERSION & const.DB_SUPPORTED_VERSION_MASK: raise exc.UnsupportedDatabaseVersion('Unsupported file version: {0}'.format(hex(self.header.version))) #Actually, only AES is supported. if not self.header.flags & HeaderStruct.AES: raise exc.UnsupportedDatabaseEncryption('Only AES encryption is supported.') final_key = util.derive_key(seed_key=self.header.seed_key, seed_rand=self.header.seed_rand, rounds=self.header.key_enc_rounds, password=password, keyfile=keyfile) # FIXME: Remove this once we've tracked down issues. self.log.debug("(load) Final key: {0!r}, pass={1}".format(final_key, password)) decrypted_content = util.decrypt_aes_cbc(crypted_content, key=final_key, iv=self.header.encryption_iv) # Check if decryption failed if ((len(decrypted_content) > const.DB_MAX_CONTENT_LEN) or (len(decrypted_content) == 0 and self.header.ngroups > 0)): raise exc.IncorrectKey("Decryption failed! The key is wrong or the file is damaged.") if not self.header.contents_hash == hashlib.sha256(decrypted_content).digest(): self.log.debug("Decrypted content: {0!r}".format(decrypted_content)) self.log.error("Hash mismatch. Header hash = {0!r}, hash of contents = {1!r}".format(self.header.contents_hash, hashlib.sha256(decrypted_content).digest())) raise exc.AuthenticationError("Hash test failed. The key is wrong or the file is damaged.") # First thing (after header) are the group definitions. for _i in range(self.header.ngroups): gstruct = GroupStruct(decrypted_content) self.groups.append(Group.from_struct(gstruct)) length = len(gstruct) decrypted_content = decrypted_content[length:] # Next come the entry definitions. for _i in range(self.header.nentries): estruct = EntryStruct(decrypted_content) self.entries.append(Entry.from_struct(estruct)) length = len(estruct) decrypted_content = decrypted_content[length:] # Sets up the hierarchy, relates the group/entry model objects. self._bind_model()
def load_from_buffer(self, buf, password=None, keyfile=None, readonly=False): """ Load a database from passed-in buffer (bytes). :param buf: A string (bytes) of the database contents. :type buf: str :param password: The password for the database. :type password: str :param keyfile: Path to a keyfile (or a stream) that can be used instead of or in conjunction with password for database. :type keyfile: str or file-like object :param readonly: Whether to open the database read-only. :type readonly: bool """ if password is None and keyfile is None: raise ValueError("Password and/or keyfile is required.") # Save these to use as defaults when saving the database self.password = password self.keyfile = keyfile # The header is 124 bytes long, the rest is content hdr_len = HeaderStruct.length header_bytes = buf[:hdr_len] crypted_content = buf[hdr_len:] self.header = HeaderStruct(header_bytes) self.log.debug("Extracted header: {0}".format(self.header)) # Check if the database is supported if self.header.version & const.DB_SUPPORTED_VERSION_MASK != const.DB_SUPPORTED_VERSION & const.DB_SUPPORTED_VERSION_MASK: raise exc.UnsupportedDatabaseVersion( 'Unsupported file version: {0}'.format(hex( self.header.version))) #Actually, only AES is supported. if not self.header.flags & HeaderStruct.AES: raise exc.UnsupportedDatabaseEncryption( 'Only AES encryption is supported.') final_key = util.derive_key(seed_key=self.header.seed_key, seed_rand=self.header.seed_rand, rounds=self.header.key_enc_rounds, password=password, keyfile=keyfile) # FIXME: Remove this once we've tracked down issues. self.log.debug("(load) Final key: {0!r}, pass={1}".format( final_key, password)) decrypted_content = util.decrypt_aes_cbc(crypted_content, key=final_key, iv=self.header.encryption_iv) # Check if decryption failed if ((len(decrypted_content) > const.DB_MAX_CONTENT_LEN) or (len(decrypted_content) == 0 and self.header.ngroups > 0)): raise exc.IncorrectKey( "Decryption failed! The key is wrong or the file is damaged.") if not self.header.contents_hash == hashlib.sha256( decrypted_content).digest(): self.log.debug( "Decrypted content: {0!r}".format(decrypted_content)) self.log.error( "Hash mismatch. Header hash = {0!r}, hash of contents = {1!r}". format(self.header.contents_hash, hashlib.sha256(decrypted_content).digest())) raise exc.AuthenticationError( "Hash test failed. The key is wrong or the file is damaged.") # First thing (after header) are the group definitions. for _i in range(self.header.ngroups): gstruct = GroupStruct(decrypted_content) self.groups.append(Group.from_struct(gstruct)) length = len(gstruct) decrypted_content = decrypted_content[length:] # Next come the entry definitions. for _i in range(self.header.nentries): estruct = EntryStruct(decrypted_content) self.entries.append(Entry.from_struct(estruct)) length = len(estruct) decrypted_content = decrypted_content[length:] # Sets up the hierarchy, relates the group/entry model objects. self._bind_model()