def _encrypt(self): """ Rebuild the master key from header settings and key-hash list. Encrypt the stream start bytes and the out-buffer formatted as hashed block stream with padding added as needed. """ # rebuild master key from (possibly) updated header self._make_master_key() # make hashed block stream block_buffer = HashedBlockIO() block_buffer.write(self.out_buffer.read()) # data is buffered in hashed block io, start a new one self.out_buffer = io.BytesIO() # write start bytes (for successful decrypt check) self.out_buffer.write(self.header.StreamStartBytes) # append blocked data to out-buffer block_buffer.write_block_stream(self.out_buffer) block_buffer.close() self.out_buffer.seek(0) # encrypt the whole thing with header settings and master key data = pad(self.out_buffer.read()) self.out_buffer = aes_cbc_encrypt(data, self.master_key, self.header.EncryptionIV)
def _decrypt(self, stream): """ Build the master key from header settings and key-hash list. Start reading from `stream` after the header and decrypt all the data. Remove padding as needed and feed into hashed block reader, set as in-buffer. """ super(KDB4File, self)._decrypt(stream) data = aes_cbc_decrypt(stream.read(), self.master_key, self.header.EncryptionIV) data = unpad(data) length = len(self.header.StreamStartBytes) if self.header.StreamStartBytes == data[:length]: # skip startbytes and wrap data in a hashed block io self.in_buffer = HashedBlockIO(bytes=data[length:]) # set successful decryption flag self.opened = True else: raise IOError('Master key invalid.')
class KDB4File(KDBFile): def __init__(self, stream=None, **credentials): self.header = KDB4Header() KDBFile.__init__(self, stream, **credentials) def set_compression(self, flag=1): """Dis- (0) or enable (default: 1) compression""" if flag not in [0, 1]: raise ValueError('Compression flag can be 0 or 1.') self.header.CompressionFlags = flag #def set_comment(self, comment): # self.header.Comment = comment def read_from(self, stream): """ Read, parse, decrypt, decompress a KeePass file from a stream. :arg stream: A file-like object (opened in 'rb' mode) or IO buffer containing a KeePass file. """ super(KDB4File, self).read_from(stream) if self.header.CompressionFlags == 1: self._unzip() def write_to(self, stream): """ Write the KeePass database back to a KeePass2 compatible file. :arg stream: A writeable file-like object or IO buffer. """ if not (isinstance(stream, io.IOBase) or isinstance(stream, file)): raise TypeError('Stream does not have the buffer interface.') self._write_header(stream) def _read_header(self, stream): """ Parse the header and write the values into self.header. Also sets self.header_length. """ # KeePass 2.07 has version 1.01, # 2.08 has 1.02, # 2.09 has 2.00, 2.10 has 2.02, 2.11 has 2.04, # 2.15 has 3.00. # The first 2 bytes are critical (i.e. loading will fail, if the # file version is too high), the last 2 bytes are informational. #TODO implement version check # the first header field starts at byte 12 after the signature stream.seek(12) while True: # field_id is a single byte field_id = stream_unpack(stream, None, 1, 'b') # field_id >10 is undefined if not field_id in self.header.fields.values(): raise IOError('Unknown header field found.') # two byte (short) length of field data length = stream_unpack(stream, None, 2, 'h') if length > 0: data = stream_unpack(stream, None, length, '{}s'.format(length)) self.header.b[field_id] = data # set position in data stream of end of header if field_id == 0: self.header_length = stream.tell() break # def _write_header(self, stream): # """Serialize the header fields from self.header into a byte stream, prefix # with file signature and version before writing header and out-buffer # to `stream`. # Note, that `stream` is flushed, but not closed!""" # # serialize header to stream # header = bytearray() # # write file signature # header.extend(struct.pack('<II', *KDB4_SIGNATURE)) # # and version # header.extend(struct.pack('<hh', 0, 3)) # field_ids = self.header.keys() # field_ids.sort() # field_ids.reverse() # field_id 0 must be last # for field_id in field_ids: # value = self.header.b[field_id] # length = len(value) # header.extend(struct.pack('<b', field_id)) # header.extend(struct.pack('<h', length)) # header.extend(struct.pack('{}s'.format(length), value)) # # write header to stream # stream.write(header) # headerHash = base64.b64encode(sha256(header)) # self.obj_root.Meta.HeaderHash = headerHash # # create HeaderHash if it does not exist # if len(self.obj_root.Meta.xpath("HeaderHash")) < 1: # etree.SubElement(self.obj_root.Meta, "HeaderHash") # # reload out_buffer because we just changed the HeaderHash # self.protect() # self.out_buffer = io.BytesIO(self.pretty_print()) # # zip or not according to header setting # if self.header.CompressionFlags == 1: # self._zip() # self._encrypt(); # # write encrypted block to stream # stream.write(self.out_buffer) # stream.flush() def _decrypt(self, stream): """ Build the master key from header settings and key-hash list. Start reading from `stream` after the header and decrypt all the data. Remove padding as needed and feed into hashed block reader, set as in-buffer. """ super(KDB4File, self)._decrypt(stream) data = aes_cbc_decrypt(stream.read(), self.master_key, self.header.EncryptionIV) data = unpad(data) length = len(self.header.StreamStartBytes) if self.header.StreamStartBytes == data[:length]: # skip startbytes and wrap data in a hashed block io self.in_buffer = HashedBlockIO(bytes=data[length:]) # set successful decryption flag self.opened = True else: raise IOError('Master key invalid.') def _encrypt(self): """ Rebuild the master key from header settings and key-hash list. Encrypt the stream start bytes and the out-buffer formatted as hashed block stream with padding added as needed. """ # rebuild master key from (possibly) updated header self._make_master_key() # make hashed block stream block_buffer = HashedBlockIO() block_buffer.write(self.out_buffer.read()) # data is buffered in hashed block io, start a new one self.out_buffer = io.BytesIO() # write start bytes (for successful decrypt check) self.out_buffer.write(self.header.StreamStartBytes) # append blocked data to out-buffer block_buffer.write_block_stream(self.out_buffer) block_buffer.close() self.out_buffer.seek(0) # encrypt the whole thing with header settings and master key data = pad(self.out_buffer.read()) self.out_buffer = aes_cbc_encrypt(data, self.master_key, self.header.EncryptionIV) def _unzip(self): """ Inplace decompress in-buffer. Read/write position is moved to 0. """ self.in_buffer.seek(0) d = zlib.decompressobj(16 + zlib.MAX_WBITS) self.in_buffer = io.BytesIO(d.decompress(self.in_buffer.read())) self.in_buffer.seek(0) def _zip(self): """ Inplace compress out-buffer. Read/write position is moved to 0. """ data = self.out_buffer.read() self.out_buffer = io.BytesIO() # note: compresslevel=6 seems to be important for kdb4! gz = gzip.GzipFile(fileobj=self.out_buffer, mode='wb', compresslevel=6) gz.write(data) gz.close() self.out_buffer.seek(0) def _make_master_key(self): """ Make the master key by (1) combining the credentials to create a composite hash, (2) transforming the hash using the transform seed for a specific number of rounds and (3) finally hashing the result in combination with the master seed. """ super(KDB4File, self)._make_master_key() composite = sha256(''.join(self.keys)) tkey = transform_key(composite, self.header.TransformSeed, self.header.TransformRounds) self.master_key = sha256(self.header.MasterSeed + tkey)
class KDB4File(KDBFile): def __init__(self, stream=None, **credentials): self.header = KDB4Header() KDBFile.__init__(self, stream, **credentials) def set_compression(self, flag=1): """Dis- (0) or enable (default: 1) compression""" if flag not in [0, 1]: raise ValueError('Compression flag can be 0 or 1.') self.header.CompressionFlags = flag #def set_comment(self, comment): # self.header.Comment = comment def read_from(self, stream): """ Read, parse, decrypt, decompress a KeePass file from a stream. :arg stream: A file-like object (opened in 'rb' mode) or IO buffer containing a KeePass file. """ super(KDB4File, self).read_from(stream) if self.header.CompressionFlags == 1: self._unzip() def write_to(self, stream): """ Write the KeePass database back to a KeePass2 compatible file. :arg stream: A writeable file-like object or IO buffer. """ if not (isinstance(stream, io.IOBase) or isinstance(stream, file)): raise TypeError('Stream does not have the buffer interface.') self._write_header(stream) def _read_header(self, stream): """ Parse the header and write the values into self.header. Also sets self.header_length. """ # KeePass 2.07 has version 1.01, # 2.08 has 1.02, # 2.09 has 2.00, 2.10 has 2.02, 2.11 has 2.04, # 2.15 has 3.00. # The first 2 bytes are critical (i.e. loading will fail, if the # file version is too high), the last 2 bytes are informational. #TODO implement version check # the first header field starts at byte 12 after the signature stream.seek(12) while True: # field_id is a single byte field_id = stream_unpack(stream, None, 1, 'b') # field_id >10 is undefined if not field_id in self.header.fields.values(): raise IOError('Unknown header field found.') # two byte (short) length of field data length = stream_unpack(stream, None, 2, 'h') if length > 0: data = stream_unpack(stream, None, length, '{}s'.format(length)) self.header.b[field_id] = data # set position in data stream of end of header if field_id == 0: self.header_length = stream.tell() break def _write_header(self, stream): """Serialize the header fields from self.header into a byte stream, prefix with file signature and version before writing header and out-buffer to `stream`. Note, that `stream` is flushed, but not closed!""" # serialize header to stream header = bytearray() # write file signature header.extend(struct.pack('<II', *KDB4_SIGNATURE)) # and version header.extend(struct.pack('<hh', 0, 3)) field_ids = self.header.keys() field_ids.sort() field_ids.reverse() # field_id 0 must be last for field_id in field_ids: value = self.header.b[field_id] length = len(value) header.extend(struct.pack('<b', field_id)) header.extend(struct.pack('<h', length)) header.extend(struct.pack('{}s'.format(length), value)) # write header to stream stream.write(header) headerHash = base64.b64encode(sha256(header)) self.obj_root.Meta.HeaderHash = headerHash # create HeaderHash if it does not exist if len(self.obj_root.Meta.xpath("HeaderHash")) < 1: etree.SubElement(self.obj_root.Meta, "HeaderHash") # reload out_buffer because we just changed the HeaderHash self.protect() self.out_buffer = io.BytesIO(self.pretty_print()) # zip or not according to header setting if self.header.CompressionFlags == 1: self._zip() self._encrypt(); # write encrypted block to stream stream.write(self.out_buffer) stream.flush() def _decrypt(self, stream): """ Build the master key from header settings and key-hash list. Start reading from `stream` after the header and decrypt all the data. Remove padding as needed and feed into hashed block reader, set as in-buffer. """ super(KDB4File, self)._decrypt(stream) data = aes_cbc_decrypt(stream.read(), self.master_key, self.header.EncryptionIV) data = unpad(data) length = len(self.header.StreamStartBytes) if self.header.StreamStartBytes == data[:length]: # skip startbytes and wrap data in a hashed block io self.in_buffer = HashedBlockIO(bytes=data[length:]) # set successful decryption flag self.opened = True else: raise IOError('Master key invalid.') def _encrypt(self): """ Rebuild the master key from header settings and key-hash list. Encrypt the stream start bytes and the out-buffer formatted as hashed block stream with padding added as needed. """ # rebuild master key from (possibly) updated header self._make_master_key() # make hashed block stream block_buffer = HashedBlockIO() block_buffer.write(self.out_buffer.read()) # data is buffered in hashed block io, start a new one self.out_buffer = io.BytesIO() # write start bytes (for successful decrypt check) self.out_buffer.write(self.header.StreamStartBytes) # append blocked data to out-buffer block_buffer.write_block_stream(self.out_buffer) block_buffer.close() self.out_buffer.seek(0) # encrypt the whole thing with header settings and master key data = pad(self.out_buffer.read()) self.out_buffer = aes_cbc_encrypt(data, self.master_key, self.header.EncryptionIV) def _unzip(self): """ Inplace decompress in-buffer. Read/write position is moved to 0. """ self.in_buffer.seek(0) d = zlib.decompressobj(16+zlib.MAX_WBITS) self.in_buffer = io.BytesIO(d.decompress(self.in_buffer.read())) self.in_buffer.seek(0) def _zip(self): """ Inplace compress out-buffer. Read/write position is moved to 0. """ data = self.out_buffer.read() self.out_buffer = io.BytesIO() # note: compresslevel=6 seems to be important for kdb4! gz = gzip.GzipFile(fileobj=self.out_buffer, mode='wb', compresslevel=6) gz.write(data) gz.close() self.out_buffer.seek(0) def _make_master_key(self): """ Make the master key by (1) combining the credentials to create a composite hash, (2) transforming the hash using the transform seed for a specific number of rounds and (3) finally hashing the result in combination with the master seed. """ super(KDB4File, self)._make_master_key() composite = sha256(''.join(self.keys)) tkey = transform_key(composite, self.header.TransformSeed, self.header.TransformRounds) self.master_key = sha256(self.header.MasterSeed + tkey)