# -*- coding: utf-8 -*- import base64 import gzip import io import xml.etree.ElementTree as ElementTree import zlib import codecs from .common import KDBFile, HeaderDictionary from .common import stream_unpack from .crypto import transform_key, pad, unpad from .crypto import xor, sha256, aes_cbc_decrypt, aes_cbc_encrypt from .hbio import HashedBlockIO from .pureSalsa20 import Salsa20 KDB4_SALSA20_IV = codecs.decode('e830094b97205d2a', 'hex') KDB4_SIGNATURE = (0x9AA2D903, 0xB54BFB67) try: file_types = (file, io.IOBase) except NameError: file_types = (io.IOBase,) class KDB4Header(HeaderDictionary): fields = { 'EndOfHeader': 0, 'Comment': 1, # cipher used for the data stream after the header 'CipherID': 2, # indicates whether decrypted data stream is gzip compressed 'CompressionFlags': 3, # 'MasterSeed': 4, # 'TransformSeed': 5, # 'TransformRounds': 6, # 'EncryptionIV': 7, # key used to protect data in xml 'ProtectedStreamKey': 8, # first 32 bytes of the decrypted data stream after the header 'StreamStartBytes': 9, # cipher used to protect data in xml (ARC4 or Salsa20) 'InnerRandomStreamID': 10, } fmt = {3: '10 is undefined if field_id not 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(' len(self._salsa_buffer): new_salsa = self.salsa.encrypt_bytes(str(bytearray(64))) self._salsa_buffer.extend(new_salsa) nacho = self._salsa_buffer[:length] del self._salsa_buffer[:length] return nacho def _unprotect(self, string): """ Base64 decode and XOR the given `string` with the next salsa. Returns an unprotected string. """ tmp = base64.b64decode(string) return str(xor(tmp, self._get_salsa(len(tmp)))) def _protect(self, string): """ XORs the given `string` with the next salsa and base64 encodes it. Returns a protected string. """ tmp = str(xor(string, self._get_salsa(len(string)))) return base64.b64encode(tmp) class KDB4Reader(KDB4File, KDBXmlExtension): """ Usually you would want to use the `keepass.open` context manager to open a file. It checks the file signature and creates a suitable reader-instance. doing it by hand is also possible:: kdb = keepass.KDB4Reader() kdb.add_credentials(password='secret') with open('passwords.kdb', 'rb') as fh: kdb.read_from(fh) or...:: with open('passwords.kdb', 'rb') as fh: kdb = keepass.KDB4Reader(fh, password='secret') """ def __init__(self, stream=None, **credentials): KDB4File.__init__(self, stream, **credentials) def read_from(self, stream, unprotect=True): KDB4File.read_from(self, stream) # the extension requires parsed header and decrypted self.in_buffer, so # initialize only here KDBXmlExtension.__init__(self, unprotect) # def write_to(self, stream, use_etree=True): # """ # Write the KeePass database back to a KeePass2 compatible file. # :arg stream: A file-like object or IO buffer. # :arg use_tree: Serialize the element tree to XML to save (default: # True), Set to False to write the data currently in the in-buffer # instead. # """ # if use_etree: # KDBXmlExtension.write_to(self, stream) # KDB4File.write_to(self, stream)