diff --git a/pykeepass/kdbx_parsing/common.py b/pykeepass/kdbx_parsing/common.py index ac42d8c0..42aabe28 100644 --- a/pykeepass/kdbx_parsing/common.py +++ b/pykeepass/kdbx_parsing/common.py @@ -13,6 +13,7 @@ Adapter, BitsSwapped, BitStruct, + Bytes, Container, Flag, GreedyBytes, @@ -31,6 +32,16 @@ log = logging.getLogger(__name__) +class RandomBytes(Bytes): + """Same as Bytes, but generate random bytes when building""" + + def _build(self, obj, stream, context, path): + length = self.length(context) if callable(self.length) else self.length + data = get_random_bytes(length) + stream_write(stream, data, length, path) + return data + + class HeaderChecksumError(Exception): pass @@ -183,7 +194,7 @@ def compute_master(context): # combine the transformed key with the header master seed to find the master_key master_key = hashlib.sha256( - context._.header.value.dynamic_header.master_seed.data + + context._.header.dynamic_header.master_seed.data + context.transformed_key).digest() return master_key @@ -312,7 +323,7 @@ class DecryptedPayload(Adapter): def _decode(self, payload_data, con, path): cipher = self.get_cipher( con.master_key, - con._.header.value.dynamic_header.encryption_iv.data + con._.header.dynamic_header.encryption_iv.data ) payload_data = cipher.decrypt(payload_data) # FIXME: Construct ugliness. Fixes #244. First 32 bytes of decrypted kdbx3 payload @@ -332,7 +343,7 @@ def _encode(self, payload_data, con, path): payload_data = self.pad(payload_data) cipher = self.get_cipher( con.master_key, - con._.header.value.dynamic_header.encryption_iv.data + con._.header.dynamic_header.encryption_iv.data ) payload_data = cipher.encrypt(payload_data) diff --git a/pykeepass/kdbx_parsing/kdbx.py b/pykeepass/kdbx_parsing/kdbx.py index 935da4ae..94ba9613 100644 --- a/pykeepass/kdbx_parsing/kdbx.py +++ b/pykeepass/kdbx_parsing/kdbx.py @@ -1,17 +1,39 @@ -from construct import Bytes, Check, Int16ul, RawCopy, Struct, Switch, this - +from construct import Bytes, Check, Int16ul, RawCopy, Struct, Switch, this, stream_seek, stream_tell, stream_read, Subconstruct from .kdbx3 import Body as Body3 from .kdbx3 import DynamicHeader as DynamicHeader3 from .kdbx4 import Body as Body4 from .kdbx4 import DynamicHeader as DynamicHeader4 + +class Copy(Subconstruct): + """Same as RawCopy, but don't create parent container when parsing. + Instead store data in ._data attribute of subconstruct, and never rebuild from data + """ + + def _parse(self, stream, context, path): + offset1 = stream_tell(stream, path) + obj = self.subcon._parsereport(stream, context, path) + offset2 = stream_tell(stream, path) + stream_seek(stream, offset1, 0, path) + obj._data = stream_read(stream, offset2 - offset1, path) + return obj + + def _build(self, obj, stream, context, path): + offset1 = stream_tell(stream, path) + obj = self.subcon._build(obj, stream, context, path) + offset2 = stream_tell(stream, path) + stream_seek(stream, offset1, 0, path) + obj._data = stream_read(stream, offset2 - offset1, path) + return obj + + # verify file signature def check_signature(ctx): return ctx.sig1 == b'\x03\xd9\xa2\x9a' and ctx.sig2 == b'\x67\xFB\x4B\xB5' KDBX = Struct( - "header" / RawCopy( + "header" / Copy( Struct( "sig1" / Bytes(4), "sig2" / Bytes(4), @@ -27,7 +49,7 @@ def check_signature(ctx): ) ), "body" / Switch( - this.header.value.major_version, + this.header.major_version, {3: Body3, 4: Body4 } diff --git a/pykeepass/kdbx_parsing/kdbx3.py b/pykeepass/kdbx_parsing/kdbx3.py index 16ef911c..facd2c18 100644 --- a/pykeepass/kdbx_parsing/kdbx3.py +++ b/pykeepass/kdbx_parsing/kdbx3.py @@ -39,6 +39,7 @@ Reparsed, TwoFishPayload, Unprotect, + RandomBytes, aes_kdf, compute_key_composite, compute_master, @@ -63,8 +64,8 @@ def compute_transformed(context): keyfile=context._._.keyfile ) transformed_key = aes_kdf( - context._.header.value.dynamic_header.transform_seed.data, - context._.header.value.dynamic_header.transform_rounds.data, + context._.header.dynamic_header.transform_seed.data, + context._.header.dynamic_header.transform_rounds.data, key_composite ) @@ -97,6 +98,7 @@ def compute_transformed(context): {'compression_flags': CompressionFlags, 'cipher_id': CipherId, 'transform_rounds': Int64ul, + 'master_seed': RandomBytes(32), 'protected_stream_id': ProtectedStreamId }, default=GreedyBytes @@ -160,16 +162,16 @@ def compute_transformed(context): # validate payload decryption "cred_check" / Checksum( Bytes(32), - lambda this: this._._.header.value.dynamic_header.stream_start_bytes.data, + lambda this: this._._.header.dynamic_header.stream_start_bytes.data, this, # exception=CredentialsError ), "xml" / Unprotect( - this._._.header.value.dynamic_header.protected_stream_id.data, - this._._.header.value.dynamic_header.protected_stream_key.data, + this._._.header.dynamic_header.protected_stream_id.data, + this._._.header.dynamic_header.protected_stream_key.data, XML( IfThenElse( - this._._.header.value.dynamic_header.compression_flags.data.compression, + this._._.header.dynamic_header.compression_flags.data.compression, Decompressed(Concatenated(PayloadBlocks)), Concatenated(PayloadBlocks) ) @@ -187,7 +189,7 @@ def compute_transformed(context): "payload" / If(this._._.decrypt, UnpackedPayload( Switch( - this._.header.value.dynamic_header.cipher_id.data, + this._.header.dynamic_header.cipher_id.data, {'aes256': AES256Payload(GreedyBytes), 'chacha20': ChaCha20Payload(GreedyBytes), 'twofish': TwoFishPayload(GreedyBytes), diff --git a/pykeepass/kdbx_parsing/kdbx4.py b/pykeepass/kdbx_parsing/kdbx4.py index 426d3ac7..1001a758 100644 --- a/pykeepass/kdbx_parsing/kdbx4.py +++ b/pykeepass/kdbx_parsing/kdbx4.py @@ -42,6 +42,7 @@ Decompressed, DynamicDict, ProtectedStreamId, + RandomBytes, Reparsed, TwoFishPayload, Unprotect, @@ -67,7 +68,7 @@ def compute_transformed(context): password=context._._.password, keyfile=context._._.keyfile ) - kdf_parameters = context._.header.value.dynamic_header.kdf_parameters.data.dict + kdf_parameters = context._.header.dynamic_header.kdf_parameters.data.dict if context._._.transformed_key is not None: transformed_key = context._._.transformed_key @@ -106,12 +107,12 @@ def compute_header_hmac_hash(context): hashlib.sha512( b'\xff' * 8 + hashlib.sha512( - context._.header.value.dynamic_header.master_seed.data + + context._.header.dynamic_header.master_seed.data + context.transformed_key + b'\x01' ).digest() ).digest(), - context._.header.data, + context._.header._data, hashlib.sha256 ).digest() @@ -173,6 +174,8 @@ def compute_header_hmac_hash(context): this.id, {'compression_flags': CompressionFlags, 'kdf_parameters': VariantDictionary, + 'master_seed': RandomBytes(32), + 'encryption_iv': RandomBytes(12), 'cipher_id': CipherId }, default=GreedyBytes @@ -198,7 +201,7 @@ def compute_payload_block_hash(this): hashlib.sha512( struct.pack('