Skip to content

Commit

Permalink
Add decrypt option to decrypt body on demand (#360)
Browse files Browse the repository at this point in the history
Fixes: #359

Co-authored-by: Jan-Michael Brummer <[email protected]>
  • Loading branch information
janbrummer and janbrummer authored Oct 9, 2023
1 parent 71c0d11 commit 8fc9708
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 28 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,9 @@ days after **credchange_date** that credential update is recommended
Miscellaneous
-------------
**read** (filename=None, password=None, keyfile=None, transformed_key=None)
**read** (filename=None, password=None, keyfile=None, transformed_key=None, decrypt=False)
where ``filename``, ``password``, and ``keyfile`` are strings. ``filename`` is the path to the database, ``password`` is the master password string, and ``keyfile`` is the path to the database keyfile. At least one of ``password`` and ``keyfile`` is required. Alternatively, the derived key can be supplied directly through ``transformed_key``.
where ``filename``, ``password``, and ``keyfile`` are strings. ``filename`` is the path to the database, ``password`` is the master password string, and ``keyfile`` is the path to the database keyfile. At least one of ``password`` and ``keyfile`` is required. Alternatively, the derived key can be supplied directly through ``transformed_key``. ``decrypt`` tells whether the file should be decrypted or not.
Can raise ``CredentialsError``, ``HeaderChecksumError``, or ``PayloadChecksumError``.
Expand Down
18 changes: 10 additions & 8 deletions pykeepass/kdbx_parsing/kdbx3.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from construct import (
Byte, Bytes, Int16ul, Int32ul, Int64ul, RepeatUntil, GreedyBytes, Struct,
this, Mapping, Switch, Prefixed, Padding, Checksum, Computed, IfThenElse,
Pointer, Tell, len_
Pointer, Tell, len_, If
)
from .common import (
aes_kdf, AES256Payload, ChaCha20Payload, TwoFishPayload, Concatenated,
Expand Down Expand Up @@ -154,13 +154,15 @@ def compute_transformed(context):
Body = Struct(
"transformed_key" / Computed(compute_transformed),
"master_key" / Computed(compute_master),
"payload" / UnpackedPayload(
Switch(
this._.header.value.dynamic_header.cipher_id.data,
{'aes256': AES256Payload(GreedyBytes),
'chacha20': ChaCha20Payload(GreedyBytes),
'twofish': TwoFishPayload(GreedyBytes),
}
"payload" / If(this._._.decrypt,
UnpackedPayload(
Switch(
this._.header.value.dynamic_header.cipher_id.data,
{'aes256': AES256Payload(GreedyBytes),
'chacha20': ChaCha20Payload(GreedyBytes),
'twofish': TwoFishPayload(GreedyBytes),
}
)
)
),
)
26 changes: 15 additions & 11 deletions pykeepass/kdbx_parsing/kdbx4.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from construct import (
Byte, Bytes, Int32ul, RepeatUntil, GreedyBytes, Struct, this, Mapping,
Switch, Flag, Prefixed, Int64ul, Int32sl, Int64sl, GreedyString, Padding,
Peek, Checksum, Computed, IfThenElse, Pointer, Tell
Peek, Checksum, Computed, IfThenElse, Pointer, Tell, If
)
from .common import (
aes_kdf, Concatenated, AES256Payload, ChaCha20Payload, TwoFishPayload,
Expand Down Expand Up @@ -259,17 +259,21 @@ def compute_payload_block_hash(this):
this._.header.data,
# exception=HeaderChecksumError,
),
"cred_check" / Checksum(
Bytes(32),
compute_header_hmac_hash,
this,
# exception=CredentialsError,
"cred_check" / If(this._._.decrypt,
Checksum(
Bytes(32),
compute_header_hmac_hash,
this,
# exception=CredentialsError,
)
),
"payload" / UnpackedPayload(
IfThenElse(
this._.header.value.dynamic_header.compression_flags.data.compression,
Decompressed(DecryptedPayload),
DecryptedPayload
"payload" / If(this._._.decrypt,
UnpackedPayload(
IfThenElse(
this._.header.value.dynamic_header.compression_flags.data.compression,
Decompressed(DecryptedPayload),
DecryptedPayload
)
)
)
)
33 changes: 26 additions & 7 deletions pykeepass/pykeepass.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class PyKeePass():
database is assumed to have no keyfile
transformed_key (:obj:`bytes`, optional): precomputed transformed
key.
decrypt (:obj:`bool`, optional): whether to decrypt XML payload.
Set `False` to access outer header information without decrypting
database.
Raises:
CredentialsError: raised when password/keyfile or transformed key
Expand All @@ -57,13 +60,14 @@ class PyKeePass():
"""

def __init__(self, filename, password=None, keyfile=None,
transformed_key=None):
transformed_key=None, decrypt=True):

self.read(
filename=filename,
password=password,
keyfile=keyfile,
transformed_key=transformed_key
transformed_key=transformed_key,
decrypt=decrypt
)

def __enter__(self):
Expand All @@ -74,7 +78,7 @@ def __exit__(self, typ, value, tb):
pass

def read(self, filename=None, password=None, keyfile=None,
transformed_key=None):
transformed_key=None, decrypt=True):
"""
See class docstring.
Expand All @@ -94,14 +98,16 @@ def read(self, filename=None, password=None, keyfile=None,
filename,
password=password,
keyfile=keyfile,
transformed_key=transformed_key
transformed_key=transformed_key,
decrypt=decrypt
)
else:
self.kdbx = KDBX.parse_file(
filename,
password=password,
keyfile=keyfile,
transformed_key=transformed_key
transformed_key=transformed_key,
decrypt=decrypt
)

except CheckError as e:
Expand Down Expand Up @@ -152,7 +158,8 @@ def save(self, filename=None, transformed_key=None):
filename,
password=self.password,
keyfile=self.keyfile,
transformed_key=transformed_key
transformed_key=transformed_key,
decrypt=True
)
else:
# save to temporary file to prevent database clobbering
Expand All @@ -164,7 +171,8 @@ def save(self, filename=None, transformed_key=None):
filename_tmp,
password=self.password,
keyfile=self.keyfile,
transformed_key=transformed_key
transformed_key=transformed_key,
decrypt=True
)
except Exception as e:
os.remove(filename_tmp)
Expand Down Expand Up @@ -207,6 +215,17 @@ def transformed_key(self):
and passed to `open` for faster database opening"""
return self.kdbx.body.transformed_key

@property
def database_salt(self):
"""bytes: salt of database kdf. This can be used for adding additional
credentials which are used in extension to current keyfile."""

if self.version == (3, 1):
return self.kdbx.header.value.dynamic_header.transform_seed.data

kdf_parameters = self.kdbx.header.value.dynamic_header.kdf_parameters.data.dict
return kdf_parameters['S'].value

@property
def tree(self):
"""lxml.etree._ElementTree: database XML payload"""
Expand Down
24 changes: 24 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,30 @@ def test_open_error(self):
os.path.join(base_dir, keyfile)
)


def test_open_no_decrypt(self):

databases = [
'test3.kdbx',
'test4.kdbx',
]
passwords = [
'invalid_password',
'invalid_password',
]
salts = [
b'\x82\xef\xf1\x05\x13\xbcQ\xa7\x8aG\x04b\xc7^o(\xf2R[\xc0\x0f\xa4?\xaa\xf9 Gi\xcf\xaf6\x0f',
b'\x82\xb0\xab/Bbn\x93\x90\xe0\x02m\x82\xaa\x9a\x9a\xd1\xc0k\x95\xbb\xc5kn\xe3\xeb\xd6GHg<$'
]
for database, password, salt in zip(databases, passwords, salts):
kp = PyKeePass(
os.path.join(base_dir, database),
password,
decrypt=False
)

self.assertEqual(kp.database_salt, salt)

if __name__ == '__main__':
unittest.main()

0 comments on commit 8fc9708

Please sign in to comment.