diff options
author | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 11:01:49 +0200 |
---|---|---|
committer | AL-LCL <alvin@alvinhavel.com> | 2023-05-19 11:01:49 +0200 |
commit | 20dbeb2f38684c65ff0a4b99012c161295708e88 (patch) | |
tree | a5b8445f55da2fbbb92443b68e9d7354a290c598 /foreign/client_handling/lazagne/softwares/memory/libkeepass |
Diffstat (limited to 'foreign/client_handling/lazagne/softwares/memory/libkeepass')
6 files changed, 1295 insertions, 0 deletions
diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/__init__.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/__init__.py new file mode 100644 index 0000000..6a6eec1 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/__init__.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import io +from contextlib import contextmanager + +from .common import read_signature +# from kdb3 import KDB3Reader, KDB3_SIGNATURE +from .kdb4 import KDB4Reader, KDB4_SIGNATURE + +BASE_SIGNATURE = 0x9AA2D903 + +_kdb_readers = { + # KDB3_SIGNATURE[1]: KDB3Reader, + #0xB54BFB66: KDB4Reader, # pre2.x may work, untested + KDB4_SIGNATURE[1]: KDB4Reader, + } + +@contextmanager +def open(filename, **credentials): + """ + A contextmanager to open the KeePass file with `filename`. Use a `password` + and/or `keyfile` named argument for decryption. + + Files are identified using their signature and a reader suitable for + the file format is intialized and returned. + + Note: `keyfile` is currently not supported for v3 KeePass files. + """ + kdb = None + try: + with io.open(filename, 'rb') as stream: + signature = read_signature(stream) + cls = get_kdb_reader(signature) + kdb = cls(stream, **credentials) + yield kdb + kdb.close() + except Exception: + if kdb: kdb.close() + raise + +def add_kdb_reader(sub_signature, cls): + """ + Add or overwrite the class used to process a KeePass file. + + KeePass uses two signatures to identify files. The base signature is + always `0x9AA2D903`. The second/sub signature varies. For example + KeePassX uses the v3 sub signature `0xB54BFB65` and KeePass2 the v4 sub + signature `0xB54BFB67`. + + Use this method to add or replace a class by givin a `sub_signature` as + integer and a class, which should be a subclass of + `keepass.common.KDBFile`. + """ + _kdb_readers[sub_signature] = cls + +def get_kdb_reader(signature): + """ + Retrieve the class used to process a KeePass file by `signature`, which + is a a tuple or list with two elements. The first being the base signature + and the second the sub signature as integers. + """ + if signature[0] != BASE_SIGNATURE: + raise IOError('Unknown base signature.') + + if signature[1] not in _kdb_readers: + raise IOError('Unknown sub signature.') + + return _kdb_readers[signature[1]] + diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/common.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/common.py new file mode 100644 index 0000000..9a78a40 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/common.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +import base64 +import codecs +import io +import struct +from xml.etree import ElementTree + +from .crypto import sha256 + +try: + file_types = (file, io.IOBase) +except NameError: + file_types = (io.IOBase,) + + +# file header +class HeaderDictionary(dict): + """ + A dictionary on steroids for comfortable header field storage and + manipulation. + + Header fields must be defined in the `fields` property before filling the + dictionary with data. The `fields` property is a simple dictionary, where + keys are field names (string) and values are field ids (int):: + + >>> h.fields['rounds'] = 4 + + Now you can set and get values using the field id or the field name + interchangeably:: + + >>> h[4] = 3000 + >>> print h['rounds'] + 3000 + >>> h['rounds'] = 6000 + >>> print h[4] + 6000 + + It is also possible to get and set data using the field name as an + attribute:: + + >>> h.rounds = 9000 + >>> print h[4] + 9000 + >>> print h.rounds + 9000 + + For some fields it is more comfortable to unpack their byte value into + a numeric or character value (eg. the transformation rounds). For those + fields add a format string to the `fmt` dictionary. Use the field id as + key:: + + >>> h.fmt[4] = '<q' + + Continue setting the value as before if you have it as a number and if you + need it as a number, get it like before. Only when you have the packed value + use a different interface:: + + >>> h.b.rounds = '\x70\x17\x00\x00\x00\x00\x00\x00' + >>> print h.b.rounds + '\x70\x17\x00\x00\x00\x00\x00\x00' + >>> print h.rounds + 6000 + + The `b` (binary?) attribute is a special way to set and get data in its + packed format, while the usual attribute or dictionary access allows + setting and getting a numeric value:: + + >>> h.rounds = 3000 + >>> print h.b.rounds + '\xb8\x0b\x00\x00\x00\x00\x00\x00' + >>> print h.rounds + 3000 + + """ + fields = {} + fmt = {} + + def __init__(self, *args): + dict.__init__(self, *args) + + def __getitem__(self, key): + if isinstance(key, int): + return dict.__getitem__(self, key) + else: + return dict.__getitem__(self, self.fields[key]) + + def __setitem__(self, key, val): + if isinstance(key, int): + dict.__setitem__(self, key, val) + else: + dict.__setitem__(self, self.fields[key], val) + + def __getattr__(self, key): + class wrap(object): + def __init__(self, d): + object.__setattr__(self, 'd', d) + + def __getitem__(self, key): + fmt = self.d.fmt.get(self.d.fields.get(key, key)) + if fmt: + return struct.pack(fmt, self.d[key]) + else: + return self.d[key] + + __getattr__ = __getitem__ + + def __setitem__(self, key, val): + fmt = self.d.fmt.get(self.d.fields.get(key, key)) + if fmt: + self.d[key] = struct.unpack(fmt, val)[0] + else: + self.d[key] = val + + __setattr__ = __setitem__ + + if key == 'b': + return wrap(self) + try: + return self.__getitem__(key) + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, val): + try: + return self.__setitem__(key, val) + except KeyError: + return dict.__setattr__(self, key, val) + + +# file baseclass +class KDBFile(object): + def __init__(self, stream=None, **credentials): + # list of hashed credentials (pre-transformation) + self.keys = [] + self.add_credentials(**credentials) + + # the buffer containing the decrypted/decompressed payload from a file + self.in_buffer = None + # the buffer filled with data for writing back to a file before + # encryption/compression + self.out_buffer = None + # position in the `in_buffer` where the payload begins + self.header_length = None + # decryption success flag, set this to true upon verification of the + # encryption masterkey. if this is True `in_buffer` must contain + # clear data. + self.opened = False + + # the raw/basic file handle, expect it to be closed after __init__! + if stream is not None: + if not isinstance(stream, io.IOBase): + raise TypeError('Stream does not have the buffer interface.') + self.read_from(stream) + + def read_from(self, stream): + if not (isinstance(stream, io.IOBase) or isinstance(stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + self._read_header(stream) + self._decrypt(stream) + + def _read_header(self, stream): + raise NotImplementedError('The _read_header method was not ' + 'implemented propertly.') + + def _decrypt(self, stream): + self._make_master_key() + # move read pointer beyond the file header + if self.header_length is None: + raise IOError('Header length unknown. Parse the header first!') + stream.seek(self.header_length) + + def write_to(self, stream): + raise NotImplementedError('The write_to() method was not implemented.') + + def add_credentials(self, **credentials): + if credentials.get('password'): + self.add_key_hash(sha256(credentials['password'])) + if credentials.get('keyfile'): + self.add_key_hash(load_keyfile(credentials['keyfile'])) + + def clear_credentials(self): + """Remove all previously set encryption key hashes.""" + self.keys = [] + + def add_key_hash(self, key_hash): + """ + Add an encryption key hash, can be a hashed password or a hashed + keyfile. Two things are important: must be SHA256 hashes and sequence is + important: first password if any, second key file if any. + """ + if key_hash is not None: + self.keys.append(key_hash) + + def _make_master_key(self): + if len(self.keys) == 0: + raise IndexError('No credentials found.') + + def close(self): + if self.in_buffer: + self.in_buffer.close() + + def read(self, n=-1): + """ + Read the decrypted and uncompressed data after the file header. + For example, in KDB4 this would be plain, utf-8 xml. + + Note that this is the source data for the lxml.objectify element tree + at `self.obj_root`. Any changes made to the parsed element tree will + NOT be reflected in that data stream! Use `self.pretty_print` to get + XML output from the element tree. + """ + if self.in_buffer: + return self.in_buffer.read(n) + + def seek(self, offset, whence=io.SEEK_SET): + if self.in_buffer: + return self.in_buffer.seek(offset, whence) + + def tell(self): + if self.in_buffer: + return self.in_buffer.tell() + + +# loading keyfiles +def load_keyfile(filename): + try: + return load_xml_keyfile(filename) + except Exception: + pass + try: + return load_plain_keyfile(filename) + except Exception: + pass + + +def load_xml_keyfile(filename): + """ + // Sample XML file: + // <?xml version="1.0" encoding="utf-8"?> + // <KeyFile> + // <Meta> + // <Version>1.00</Version> + // </Meta> + // <Key> + // <Data>ySFoKuCcJblw8ie6RkMBdVCnAf4EedSch7ItujK6bmI=</Data> + // </Key> + // </KeyFile> + """ + with open(filename, 'r') as f: + # ignore meta, currently there is only version "1.00" + tree = ElementTree.parse(f).getroot() + # read text from key, data and convert from base64 + return base64.b64decode(tree.find('Key/Data').text) + # raise IOError('Could not parse XML keyfile.') + + +def load_plain_keyfile(filename): + """ + A "plain" keyfile is a file containing only the key. + Any other file (JPEG, MP3, ...) can also be used as keyfile. + """ + with open(filename, 'rb') as f: + key = f.read() + # if the length is 32 bytes we assume it is the key + if len(key) == 32: + return key + # if the length is 64 bytes we assume the key is hex encoded + if len(key) == 64: + return codecs.decode(key, 'hex') + # anything else may be a file to hash for the key + return sha256(key) + # raise IOError('Could not read keyfile.') + + +def stream_unpack(stream, offset, length, typecode='I'): + if offset is not None: + stream.seek(offset) + data = stream.read(length) + return struct.unpack('<' + typecode, data)[0] + + +def read_signature(stream): + sig1 = stream_unpack(stream, 0, 4) + sig2 = stream_unpack(stream, None, 4) + # ver_minor = stream_unpack(stream, None, 2, 'h') + # ver_major = stream_unpack(stream, None, 2, 'h') + # return (sig1, sig2, ver_major, ver_minor) + return sig1, sig2 diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/crypto.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/crypto.py new file mode 100644 index 0000000..3e7ad67 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/crypto.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +import hashlib +import struct + +from foreign.client_handling.lazagne.config.crypto.pyaes.aes import AESModeOfOperationECB, AESModeOfOperationCBC +from foreign.client_handling.lazagne.config.winstructure import char_to_int + +AES_BLOCK_SIZE = 16 + + +def sha256(s): + """Return SHA256 digest of the string `s`.""" + return hashlib.sha256(s).digest() + + +def transform_key(key, seed, rounds): + """Transform `key` with `seed` `rounds` times using AES ECB.""" + # create transform cipher with transform seed + cipher = AESModeOfOperationECB(seed) + # transform composite key rounds times + for n in range(0, rounds): + key = b"".join([cipher.encrypt(key[i:i + AES_BLOCK_SIZE]) for i in range(0, len(key), AES_BLOCK_SIZE)]) + # return hash of transformed key + return sha256(key) + + +def aes_cbc_decrypt(data, key, enc_iv): + """Decrypt and return `data` with AES CBC.""" + cipher = AESModeOfOperationCBC(key, iv=enc_iv) + return b"".join([cipher.decrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + + +def aes_cbc_encrypt(data, key, enc_iv): + cipher = AESModeOfOperationCBC(key, iv=enc_iv) + return b"".join([cipher.encrypt(data[i:i + AES_BLOCK_SIZE]) for i in range(0, len(data), AES_BLOCK_SIZE)]) + + +def unpad(data): + extra = char_to_int(data[-1]) + return data[:len(data) - extra] + + +def pad(s): + n = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE + return s + n * struct.pack('b', n) + + +def xor(aa, bb): + """Return a bytearray of a bytewise XOR of `aa` and `bb`.""" + result = bytearray() + for a, b in zip(bytearray(aa), bytearray(bb)): + result.append(a ^ b) + return result diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/hbio.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/hbio.py new file mode 100644 index 0000000..ade6e96 --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/hbio.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +import hashlib +import io +import struct + +# default from KeePass2 source +BLOCK_LENGTH = 1024 * 1024 + +try: + file_types = (file, io.IOBase) +except NameError: + file_types = (io.IOBase,) + +# HEADER_LENGTH = 4+32+4 + +def read_int(stream, length): + try: + return struct.unpack('<I', stream.read(length))[0] + except Exception: + return None + + +class HashedBlockIO(io.BytesIO): + """ + The data is stored in hashed blocks. Each block consists of a block index (4 + bytes), the hash (32 bytes) and the block length (4 bytes), followed by the + block data. The block index starts counting at 0. The block hash is a + SHA-256 hash of the block data. A block has a maximum length of + BLOCK_LENGTH, but can be shorter. + + Provide a I/O stream containing the hashed block data as the `block_stream` + argument when creating a HashedBlockReader. Alternatively the `bytes` + argument can be used to hand over data as a string/bytearray/etc. The data + is verified upon initialization and an IOError is raised when a hash does + not match. + + HashedBlockReader is a subclass of io.BytesIO. The inherited read, seek, ... + functions shall be used to access the verified data. + """ + + def __init__(self, block_stream=None, bytes=None): + io.BytesIO.__init__(self) + input_stream = None + if block_stream is not None: + if not (isinstance(block_stream, io.IOBase) or isinstance(block_stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + input_stream = block_stream + elif bytes is not None: + input_stream = io.BytesIO(bytes) + if input_stream is not None: + self.read_block_stream(input_stream) + + def read_block_stream(self, block_stream): + """ + Read the whole block stream into the self-BytesIO. + """ + if not (isinstance(block_stream, io.IOBase) or isinstance(block_stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + while True: + data = self._next_block(block_stream) + if not self.write(data): + break + self.seek(0) + + def _next_block(self, block_stream): + """ + Read the next block and verify the data. + Raises an IOError if the hash does not match. + """ + index = read_int(block_stream, 4) + bhash = block_stream.read(32) + length = read_int(block_stream, 4) + + if length > 0: + data = block_stream.read(length) + if hashlib.sha256(data).digest() == bhash: + return data + else: + raise IOError('Block hash mismatch error.') + return bytes() + + def write_block_stream(self, stream, block_length=BLOCK_LENGTH): + """ + Write all data in this buffer, starting at stream position 0, formatted + in hashed blocks to the given `stream`. + + For example, writing data from one file into another as hashed blocks:: + + # create new hashed block io without input stream or data + hb = HashedBlockIO() + # read from a file, write into the empty hb + with open('sample.dat', 'rb') as infile: + hb.write(infile.read()) + # write from the hb into a new file + with open('hb_sample.dat', 'w') as outfile: + hb.write_block_stream(outfile) + """ + if not (isinstance(stream, io.IOBase) or isinstance(stream, file_types)): + raise TypeError('Stream does not have the buffer interface.') + index = 0 + self.seek(0) + while True: + data = self.read(block_length) + if data: + stream.write(struct.pack('<I', index)) + stream.write(hashlib.sha256(data).digest()) + stream.write(struct.pack('<I', len(data))) + stream.write(data) + index += 1 + else: + stream.write(struct.pack('<I', index)) + stream.write('\x00' * 32) + stream.write(struct.pack('<I', 0)) + break diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/kdb4.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/kdb4.py new file mode 100644 index 0000000..b40760e --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/kdb4.py @@ -0,0 +1,419 @@ +# -*- 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: '<I', 6: '<q'} + + +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_types)): + # 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 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('<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 KDBXmlExtension: + """ + The KDB4 payload is a XML document. For easier use this class provides + a lxml.objectify'ed version of the XML-tree as the `obj_root` attribute. + + More importantly though in the XML document text values can be protected + using Salsa20. Protected elements are unprotected by default (passwords are + in clear). You can override this with the `unprotect=False` argument. + """ + + def __init__(self, unprotect=True): + self._salsa_buffer = bytearray() + self.salsa = Salsa20( + sha256(self.header.ProtectedStreamKey), + KDB4_SALSA20_IV) + + self.in_buffer.seek(0) + # self.tree = objectify.parse(self.in_buffer) + # self.obj_root = self.tree.getroot() + self.obj_root = ElementTree.fromstring(self.in_buffer.read()) + + if unprotect: + self.unprotect() + + def unprotect(self): + """ + Find all elements with a 'Protected=True' attribute and replace the text + with an unprotected value in the XML element tree. The original text is + set as 'ProtectedValue' attribute and the 'Protected' attribute is set + to 'False'. The 'ProtectPassword' element in the 'Meta' section is also + set to 'False'. + """ + self._reset_salsa() + for elem in self.obj_root.iterfind('.//Value[@Protected="True"]'): + if elem.text is not None: + elem.set('ProtectedValue', elem.text) + elem.set('Protected', 'False') + elem.text = self._unprotect(elem.text) + + # def protect(self): + # """ + # Find all elements with a 'Protected=False' attribute and replace the + # text with a protected value in the XML element tree. If there was a + # 'ProtectedValue' attribute, it is deleted and the 'Protected' attribute + # is set to 'True'. The 'ProtectPassword' element in the 'Meta' section is + # also set to 'True'. + + # This does not just restore the previous protected value, but reencrypts + # all text values of elements with 'Protected=False'. So you could use + # this after modifying a password, adding a completely new entry or + # deleting entry history items. + # """ + # self._reset_salsa() + # self.obj_root.Meta.MemoryProtection.ProtectPassword._setText('True') + # for elem in self.obj_root.iterfind('.//Value[@Protected="False"]'): + # etree.strip_attributes(elem, 'ProtectedValue') + # elem.set('Protected', 'True') + # elem._setText(self._protect(elem.text)) + + # def pretty_print(self): + # """Return a serialization of the element tree.""" + # return etree.tostring(self.obj_root, pretty_print=True, + # encoding='utf-8', standalone=True) + + def to_dic(self): + """Return a dictionnary of the element tree.""" + pwd_found = [] + # print etree.tostring(self.obj_root) + root = ElementTree.fromstring(ElementTree.tostring(self.obj_root)) + for entry in root.findall('.//Root//Entry'): + dic = {} + for elem in entry.iter('String'): + try: + if elem[0].text == 'UserName': + dic['Login'] = elem[1].text + else: + # Replace new line by a point + dic[elem[0].text] = elem[1].text.replace('\n', '.') + except Exception as e: + # print e + pass + pwd_found.append(dic) + return pwd_found + + # def write_to(self, stream): + # """Serialize the element tree to the out-buffer.""" + # if self.out_buffer is None: + # self.protect() + # self.out_buffer = io.BytesIO(self.pretty_print()) + + def _reset_salsa(self): + """Clear the salsa buffer and reset algorithm counter to 0.""" + self._salsa_buffer = bytearray() + self.salsa.set_counter(0) + + def _get_salsa(self, length): + """ + Returns the next section of the "random" Salsa20 bytes with the + requested `length`. + """ + while length > 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) diff --git a/foreign/client_handling/lazagne/softwares/memory/libkeepass/pureSalsa20.py b/foreign/client_handling/lazagne/softwares/memory/libkeepass/pureSalsa20.py new file mode 100644 index 0000000..ae0641a --- /dev/null +++ b/foreign/client_handling/lazagne/softwares/memory/libkeepass/pureSalsa20.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" + pureSalsa20.py -- a pure Python implementation of the Salsa20 cipher + ==================================================================== + There are comments here by two authors about three pieces of software: + comments by Larry Bugbee about + Salsa20, the stream cipher by Daniel J. Bernstein + (including comments about the speed of the C version) and + pySalsa20, Bugbee's own Python wrapper for salsa20.c + (including some references), and + comments by Steve Witham about + pureSalsa20, Witham's pure Python 2.5 implementation of Salsa20, + which follows pySalsa20's API, and is in this file. + + Salsa20: a Fast Streaming Cipher (comments by Larry Bugbee) + ----------------------------------------------------------- + + Salsa20 is a fast stream cipher written by Daniel Bernstein + that basically uses a hash function and XOR making for fast + encryption. (Decryption uses the same function.) Salsa20 + is simple and quick. + + Some Salsa20 parameter values... + design strength 128 bits + key length 128 or 256 bits, exactly + IV, aka nonce 64 bits, always + chunk size must be in multiples of 64 bytes + + Salsa20 has two reduced versions, 8 and 12 rounds each. + + One benchmark (10 MB): + 1.5GHz PPC G4 102/97/89 MB/sec for 8/12/20 rounds + AMD Athlon 2500+ 77/67/53 MB/sec for 8/12/20 rounds + (no I/O and before Python GC kicks in) + + Salsa20 is a Phase 3 finalist in the EU eSTREAM competition + and appears to be one of the fastest ciphers. It is well + documented so I will not attempt any injustice here. Please + see "References" below. + + ...and Salsa20 is "free for any use". + + + pySalsa20: a Python wrapper for Salsa20 (Comments by Larry Bugbee) + ------------------------------------------------------------------ + + pySalsa20.py is a simple ctypes Python wrapper. Salsa20 is + as it's name implies, 20 rounds, but there are two reduced + versions, 8 and 12 rounds each. Because the APIs are + identical, pySalsa20 is capable of wrapping all three + versions (number of rounds hardcoded), including a special + version that allows you to set the number of rounds with a + set_rounds() function. Compile the version of your choice + as a shared library (not as a Python extension), name and + install it as libsalsa20.so. + + Sample usage: + from pySalsa20 import Salsa20 + s20 = Salsa20(key, IV) + dataout = s20.encryptBytes(datain) # same for decrypt + + This is EXPERIMENTAL software and intended for educational + purposes only. To make experimentation less cumbersome, + pySalsa20 is also free for any use. + + THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF + ANY KIND. USE AT YOUR OWN RISK. + + Enjoy, + + Larry Bugbee + bugbee@seanet.com + April 2007 + + + References: + ----------- + http://en.wikipedia.org/wiki/Salsa20 + http://en.wikipedia.org/wiki/Daniel_Bernstein + http://cr.yp.to/djb.html + http://www.ecrypt.eu.org/stream/salsa20p3.html + http://www.ecrypt.eu.org/stream/p3ciphers/salsa20/salsa20_p3source.zip + + + Prerequisites for pySalsa20: + ---------------------------- + - Python 2.5 (haven't tested in 2.4) + + + pureSalsa20: Salsa20 in pure Python 2.5 (comments by Steve Witham) + ------------------------------------------------------------------ + + pureSalsa20 is the stand-alone Python code in this file. + It implements the underlying Salsa20 core algorithm + and emulates pySalsa20's Salsa20 class API (minus a bug(*)). + + pureSalsa20 is MUCH slower than libsalsa20.so wrapped with pySalsa20-- + about 1/1000 the speed for Salsa20/20 and 1/500 the speed for Salsa20/8, + when encrypting 64k-byte blocks on my computer. + + pureSalsa20 is for cases where portability is much more important than + speed. I wrote it for use in a "structured" random number generator. + + There are comments about the reasons for this slowness in + http://www.tiac.net/~sw/2010/02/PureSalsa20 + + Sample usage: + from pureSalsa20 import Salsa20 + s20 = Salsa20(key, IV) + dataout = s20.encryptBytes(datain) # same for decrypt + + I took the test code from pySalsa20, added a bunch of tests including + rough speed tests, and moved them into the file testSalsa20.py. + To test both pySalsa20 and pureSalsa20, type + python testSalsa20.py + + (*)The bug (?) in pySalsa20 is this. The rounds variable is global to the + libsalsa20.so library and not switched when switching between instances + of the Salsa20 class. + s1 = Salsa20( key, IV, 20 ) + s2 = Salsa20( key, IV, 8 ) + In this example, + with pySalsa20, both s1 and s2 will do 8 rounds of encryption. + with pureSalsa20, s1 will do 20 rounds and s2 will do 8 rounds. + Perhaps giving each instance its own nRounds variable, which + is passed to the salsa20wordtobyte() function, is insecure. I'm not a + cryptographer. + + pureSalsa20.py and testSalsa20.py are EXPERIMENTAL software and + intended for educational purposes only. To make experimentation less + cumbersome, pureSalsa20.py and testSalsa20.py are free for any use. + + Revisions: + ---------- + p3.2 Fixed bug that initialized the output buffer with plaintext! + Saner ramping of nreps in speed test. + Minor changes and print statements. + p3.1 Took timing variability out of add32() and rot32(). + Made the internals more like pySalsa20/libsalsa . + Put the semicolons back in the main loop! + In encryptBytes(), modify a byte array instead of appending. + Fixed speed calculation bug. + Used subclasses instead of patches in testSalsa20.py . + Added 64k-byte messages to speed test to be fair to pySalsa20. + p3 First version, intended to parallel pySalsa20 version 3. + + More references: + ---------------- + http://www.seanet.com/~bugbee/crypto/salsa20/ [pySalsa20] + http://cr.yp.to/snuffle.html [The original name of Salsa20] + http://cr.yp.to/snuffle/salsafamily-20071225.pdf [ Salsa20 design] + http://www.tiac.net/~sw/2010/02/PureSalsa20 + + THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF + ANY KIND. USE AT YOUR OWN RISK. + + Cheers, + + Steve Witham sw at remove-this tiac dot net + February, 2010 +""" + +from array import array +from struct import Struct +from foreign.client_handling.lazagne.config.winstructure import char_to_int + +little_u64 = Struct("<Q") # little-endian 64-bit unsigned. +# Unpacks to a tuple of one element! + +little16_i32 = Struct("<16i") # 16 little-endian 32-bit signed ints. +little4_i32 = Struct("<4i") # 4 little-endian 32-bit signed ints. +little2_i32 = Struct("<2i") # 2 little-endian 32-bit signed ints. + +_version = 'p3.2' + +try: + long + xrange +except NameError: + long = int + xrange = range + + +# ----------- Salsa20 class which emulates pySalsa20.Salsa20 --------------- + +class Salsa20(object): + def __init__(self, key=None, iv=None, rounds=20): + self._lastChunk64 = True + self._IVbitlen = 64 # must be 64 bits + self.ctx = [0] * 16 + if key: + self.set_key(key) + if iv: + self.set_iv(iv) + + self.set_rounds(rounds) + + def set_key(self, key): + assert type(key) == str + ctx = self.ctx + if len(key) == 32: # recommended + constants = "expand 32-byte k" + ctx[1], ctx[2], ctx[3], ctx[4] = little4_i32.unpack(key[0:16]) + ctx[11], ctx[12], ctx[13], ctx[14] = little4_i32.unpack(key[16:32]) + elif len(key) == 16: + constants = "expand 16-byte k" + ctx[1], ctx[2], ctx[3], ctx[4] = little4_i32.unpack(key[0:16]) + ctx[11], ctx[12], ctx[13], ctx[14] = little4_i32.unpack(key[0:16]) + else: + raise Exception("key length isn't 32 or 16 bytes.") + ctx[0], ctx[5], ctx[10], ctx[15] = little4_i32.unpack(constants) + + def set_iv(self, iv): + assert type(iv) == str + assert len(iv) * 8 == 64, 'nonce (IV) not 64 bits' + self.iv = iv + ctx = self.ctx + ctx[6], ctx[7] = little2_i32.unpack(iv) + ctx[8], ctx[9] = 0, 0 # Reset the block counter. + + set_nonce = set_iv # support an alternate name + + def set_counter(self, counter): + assert (type(counter) in (int, long)) + assert (0 <= counter < 1 << 64), "counter < 0 or >= 2**64" + ctx = self.ctx + ctx[8], ctx[9] = little2_i32.unpack(little_u64.pack(counter)) + + def get_counter(self): + return little_u64.unpack(little2_i32.pack(*self.ctx[8:10]))[0] + + def set_rounds(self, rounds, testing=False): + assert testing or rounds in [8, 12, 20], 'rounds must be 8, 12, 20' + self.rounds = rounds + + def encrypt_bytes(self, data): + assert type(data) == str, 'data must be byte string' + assert self._lastChunk64, 'previous chunk not multiple of 64 bytes' + lendata = len(data) + munged = array('c', '\x00' * lendata) + for i in xrange(0, lendata, 64): + h = salsa20_wordtobyte(self.ctx, self.rounds, check_rounds=False) + self.set_counter((self.get_counter() + 1) % 2 ** 64) + # Stopping at 2^70 bytes per nonce is user's responsibility. + for j in xrange(min(64, lendata - i)): + munged[i + j] = chr(char_to_int(data[i + j]) ^ char_to_int(h[j])) + + self._lastChunk64 = not lendata % 64 + return munged.tostring() + + decrypt_bytes = encrypt_bytes # encrypt and decrypt use same function + + +# -------------------------------------------------------------------------- + +def salsa20_wordtobyte(input, n_rounds=20, check_rounds=True): + """ Do nRounds Salsa20 rounds on a copy of + input: list or tuple of 16 ints treated as little-endian unsigneds. + Returns a 64-byte string. + """ + + assert (type(input) in (list, tuple) and len(input) == 16) + assert (not check_rounds or (n_rounds in [8, 12, 20])) + + x = list(input) + + def XOR(a, b): + return a ^ b + + ROTATE = rot32 + PLUS = add32 + + for i in range(n_rounds / 2): + # These ...XOR...ROTATE...PLUS... lines are from ecrypt-linux.c + # unchanged except for indents and the blank line between rounds: + x[4] = XOR(x[4], ROTATE(PLUS(x[0], x[12]), 7)) + x[8] = XOR(x[8], ROTATE(PLUS(x[4], x[0]), 9)) + x[12] = XOR(x[12], ROTATE(PLUS(x[8], x[4]), 13)) + x[0] = XOR(x[0], ROTATE(PLUS(x[12], x[8]), 18)) + x[9] = XOR(x[9], ROTATE(PLUS(x[5], x[1]), 7)) + x[13] = XOR(x[13], ROTATE(PLUS(x[9], x[5]), 9)) + x[1] = XOR(x[1], ROTATE(PLUS(x[13], x[9]), 13)) + x[5] = XOR(x[5], ROTATE(PLUS(x[1], x[13]), 18)) + x[14] = XOR(x[14], ROTATE(PLUS(x[10], x[6]), 7)) + x[2] = XOR(x[2], ROTATE(PLUS(x[14], x[10]), 9)) + x[6] = XOR(x[6], ROTATE(PLUS(x[2], x[14]), 13)) + x[10] = XOR(x[10], ROTATE(PLUS(x[6], x[2]), 18)) + x[3] = XOR(x[3], ROTATE(PLUS(x[15], x[11]), 7)) + x[7] = XOR(x[7], ROTATE(PLUS(x[3], x[15]), 9)) + x[11] = XOR(x[11], ROTATE(PLUS(x[7], x[3]), 13)) + x[15] = XOR(x[15], ROTATE(PLUS(x[11], x[7]), 18)) + + x[1] = XOR(x[1], ROTATE(PLUS(x[0], x[3]), 7)) + x[2] = XOR(x[2], ROTATE(PLUS(x[1], x[0]), 9)) + x[3] = XOR(x[3], ROTATE(PLUS(x[2], x[1]), 13)) + x[0] = XOR(x[0], ROTATE(PLUS(x[3], x[2]), 18)) + x[6] = XOR(x[6], ROTATE(PLUS(x[5], x[4]), 7)) + x[7] = XOR(x[7], ROTATE(PLUS(x[6], x[5]), 9)) + x[4] = XOR(x[4], ROTATE(PLUS(x[7], x[6]), 13)) + x[5] = XOR(x[5], ROTATE(PLUS(x[4], x[7]), 18)) + x[11] = XOR(x[11], ROTATE(PLUS(x[10], x[9]), 7)) + x[8] = XOR(x[8], ROTATE(PLUS(x[11], x[10]), 9)) + x[9] = XOR(x[9], ROTATE(PLUS(x[8], x[11]), 13)) + x[10] = XOR(x[10], ROTATE(PLUS(x[9], x[8]), 18)) + x[12] = XOR(x[12], ROTATE(PLUS(x[15], x[14]), 7)) + x[13] = XOR(x[13], ROTATE(PLUS(x[12], x[15]), 9)) + x[14] = XOR(x[14], ROTATE(PLUS(x[13], x[12]), 13)) + x[15] = XOR(x[15], ROTATE(PLUS(x[14], x[13]), 18)) + + for i in range(len(input)): + x[i] = PLUS(x[i], input[i]) + return little16_i32.pack(*x) + + +# --------------------------- 32-bit ops ------------------------------- + +def trunc32(w): + """ Return the bottom 32 bits of w as a Python int. + This creates longs temporarily, but returns an int. """ + w = int((w & 0x7fffFFFF) | -(w & 0x80000000)) + assert type(w) == int + return w + + +def add32(a, b): + """ Add two 32-bit words discarding carry above 32nd bit, + and without creating a Python long. + Timing shouldn't vary. + """ + lo = (a & 0xFFFF) + (b & 0xFFFF) + hi = (a >> 16) + (b >> 16) + (lo >> 16) + return (-(hi & 0x8000) | (hi & 0x7FFF)) << 16 | (lo & 0xFFFF) + + +def rot32(w, n_left): + """ Rotate 32-bit word left by nLeft or right by -nLeft + without creating a Python long. + Timing depends on nLeft but not on w. + """ + n_left &= 31 # which makes nLeft >= 0 + if n_left == 0: + return w + + # Note: now 1 <= nLeft <= 31. + # RRRsLLLLLL There are nLeft RRR's, (31-nLeft) LLLLLL's, + # => sLLLLLLRRR and one s which becomes the sign bit. + RRR = (((w >> 1) & 0x7fffFFFF) >> (31 - n_left)) + sLLLLLL = -((1 << (31 - n_left)) & w) | (0x7fffFFFF >> n_left) & w + return RRR | (sLLLLLL << n_left) + +# --------------------------------- end ----------------------------------- |