Skip to content

Commit

Permalink
Allow keyfile to be passed as bytes
Browse files Browse the repository at this point in the history
In order to prevent application to create temporary files,
provide keyfile as bytes.

Fixes: #363
  • Loading branch information
Jan-Michael Brummer committed Oct 23, 2023
1 parent 8fc9708 commit 75fe4a1
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 36 deletions.
81 changes: 45 additions & 36 deletions pykeepass/kdbx_parsing/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ 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 @@ -116,43 +135,33 @@ def compute_key_composite(password=None, keyfile=None):
password_composite = b''
# hash the keyfile
if keyfile:
# try to read XML keyfile
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):
if isinstance(keyfile, bytes):
keyfile_composite = keyfile_composite = load_keyfile_composite(keyfile)
else:
# try to read XML keyfile
try:
with open(keyfile, 'rb') as f:
key = f.read()

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()
except:
raise IOError('Could not read keyfile')
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')

else:
keyfile_composite = b''
Expand Down
23 changes: 23 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,29 @@ def test_open_no_decrypt(self):

self.assertEqual(kp.database_salt, salt)


def test_keyfile_as_bytes(self):

databases = [
'test4.kdbx',
]
passwords = [
'password',
]
keyfiles = [
base_dir + '/test4.key'
]
for database, password, keyfile in zip(databases, passwords, keyfiles):
with open(keyfile, "rb") as fh:
buf = fh.read()

kp = PyKeePass(
os.path.join(base_dir, database),
password,
keyfile=buf
)


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

0 comments on commit 75fe4a1

Please sign in to comment.