Skip to content

Commit

Permalink
support file-like keyfile on read()
Browse files Browse the repository at this point in the history
  • Loading branch information
Evidlo committed Nov 27, 2023
1 parent 75fe4a1 commit c53ecd0
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 131 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ Miscellaneous
-------------
**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``. ``decrypt`` tells whether the file should be decrypted or not.
where ``filename``, ``password``, and ``keyfile`` are strings ( ``filename`` and ``keyfile`` may also be file-like objects). ``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 All @@ -376,7 +376,7 @@ reload database from disk using previous credentials
**save** (filename=None)
where ``filename`` is the path of the file to save to. If ``filename`` is not given, the path given in ``read`` will be used.
where ``filename`` is the path of the file to save to (``filename`` may also be file-like object). If ``filename`` is not given, the path given in ``read`` will be used.
**password**
Expand Down
78 changes: 35 additions & 43 deletions pykeepass/kdbx_parsing/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,6 @@ def aes_kdf(key, rounds, key_composite):
return hashlib.sha256(transformed_key).digest()


def load_keyfile_composite(key: bytes):
try:
int(key, 16)
is_hex = True
except ValueError:
is_hex = False
# if the length is 32 bytes we assume it is the key
if len(key) == 32:
keyfile_composite = key
# if the length is 64 bytes we assume the key is hex encoded
elif len(key) == 64 and is_hex:
keyfile_composite = codecs.decode(key, 'hex')
# anything else may be a file to hash for the key
else:
keyfile_composite = hashlib.sha256(key).digest()

return keyfile_composite


def compute_key_composite(password=None, keyfile=None):
"""Compute composite key.
Used in header verification and payload decryption."""
Expand All @@ -135,33 +116,44 @@ def compute_key_composite(password=None, keyfile=None):
password_composite = b''
# hash the keyfile
if keyfile:
if isinstance(keyfile, bytes):
keyfile_composite = keyfile_composite = load_keyfile_composite(keyfile)
if hasattr(keyfile, "read"):
keyfile_bytes = keyfile.read()
else:
# try to read XML keyfile
with open(keyfile, 'rb') as f:
keyfile_bytes = f.read()
# try to read XML keyfile
try:
tree = etree.fromstring(keyfile_bytes)
version = tree.find('Meta/Version').text
data_element = tree.find('Key/Data')
if version.startswith('1.0'):
keyfile_composite = base64.b64decode(data_element.text)
elif version.startswith('2.0'):
# read keyfile data and convert to bytes
keyfile_composite = bytes.fromhex(data_element.text.strip())
# validate bytes against hash
hash = bytes.fromhex(data_element.attrib['Hash'])
hash_computed = hashlib.sha256(keyfile_composite).digest()[:4]
assert hash == hash_computed, "Keyfile has invalid hash"
# otherwise, try to read plain keyfile
except (etree.XMLSyntaxError, UnicodeDecodeError):
try:
with open(keyfile, 'r') as f:
tree = etree.parse(f).getroot()
version = tree.find('Meta/Version').text
data_element = tree.find('Key/Data')
if version.startswith('1.0'):
keyfile_composite = base64.b64decode(data_element.text)
elif version.startswith('2.0'):
# read keyfile data and convert to bytes
keyfile_composite = bytes.fromhex(data_element.text.strip())
# validate bytes against hash
hash = bytes.fromhex(data_element.attrib['Hash'])
hash_computed = hashlib.sha256(keyfile_composite).digest()[:4]
assert hash == hash_computed, "Keyfile has invalid hash"
# otherwise, try to read plain keyfile
except (etree.XMLSyntaxError, UnicodeDecodeError):
try:
with open(keyfile, 'rb') as f:
key = f.read()

keyfile_composite = load_keyfile_composite(key)
except:
raise IOError('Could not read keyfile')
int(keyfile_bytes, 16)
is_hex = True
except ValueError:
is_hex = False
# if the length is 32 bytes we assume it is the key
if len(keyfile_bytes) == 32:
keyfile_composite = keyfile_bytes
# if the length is 64 bytes we assume the key is hex encoded
elif len(keyfile_bytes) == 64 and is_hex:
keyfile_composite = codecs.decode(keyfile_bytes, 'hex')
# anything else may be a file to hash for the key
else:
keyfile_composite = hashlib.sha256(keyfile_bytes).digest()
except:
raise IOError('Could not read keyfile')

else:
keyfile_composite = b''
Expand Down
Loading

0 comments on commit c53ecd0

Please sign in to comment.