diff --git a/pykeepass/baseelement.py b/pykeepass/baseelement.py index 4a5230e6..2ca238fa 100644 --- a/pykeepass/baseelement.py +++ b/pykeepass/baseelement.py @@ -2,7 +2,7 @@ import uuid from lxml import etree from lxml.builder import E -from datetime import datetime +from datetime import datetime, timezone class BaseElement(): @@ -17,9 +17,9 @@ def __init__(self, element, kp=None, icon=None, expires=False, ) if icon: self._element.append(E.IconID(icon)) - current_time_str = self._kp._encode_time(datetime.now()) + current_time_str = self._kp._encode_time(datetime.now(timezone.utc)) if expiry_time: - expiry_time_str = self._kp._encode_time(expiry_time) + expiry_time_str = self._kp._encode_time(expiry_time.astimezone(timezone.utc)) else: expiry_time_str = current_time_str @@ -116,8 +116,8 @@ def expires(self, value): def expired(self): if self.expires: return ( - self._kp._datetime_to_utc(datetime.utcnow()) > - self._kp._datetime_to_utc(self.expiry_time) + datetime.now(timezone.utc) > + self.expiry_time ) return False @@ -178,7 +178,7 @@ def touch(self, modify=False): Args: modify (bool): update access time as well a modification time """ - now = datetime.now() + now = datetime.now(timezone.utc) self.atime = now if modify: self.mtime = now diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 761828b4..93eb71e9 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -10,8 +10,7 @@ from binascii import Error as BinasciiError from construct import Container, ChecksumError, CheckError -from dateutil import parser, tz -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from lxml import etree from lxml.builder import E from pathlib import Path @@ -29,7 +28,7 @@ BLANK_DATABASE_FILENAME = "blank_database.kdbx" BLANK_DATABASE_LOCATION = os.path.join(os.path.dirname(os.path.realpath(__file__)), BLANK_DATABASE_FILENAME) BLANK_DATABASE_PASSWORD = "password" - +DT_ISOFORMAT = "%Y-%m-%dT%H:%M:%S%fZ" class PyKeePass(): """Open a KeePass database @@ -707,7 +706,7 @@ def password(self): @password.setter def password(self, password): self._password = password - self.credchange_date = datetime.now() + self.credchange_date = datetime.now(timezone.utc) @property def keyfile(self): @@ -717,7 +716,7 @@ def keyfile(self): @keyfile.setter def keyfile(self, keyfile): self._keyfile = keyfile - self.credchange_date = datetime.now() + self.credchange_date = datetime.now(timezone.utc) @property def credchange_required_days(self): @@ -754,8 +753,8 @@ def credchange_date(self): @credchange_date.setter def credchange_date(self, date): - time = self._xpath('/KeePassFile/Meta/MasterKeyChanged', first=True) - time.text = self._encode_time(date) + mk_time = self._xpath('/KeePassFile/Meta/MasterKeyChanged', first=True) + mk_time.text = self._encode_time(date) @property def credchange_required(self): @@ -763,7 +762,7 @@ def credchange_required(self): change_date = self.credchange_date if change_date is None or self.credchange_required_days == -1: return False - now_date = self._datetime_to_utc(datetime.now()) + now_date = datetime.now(timezone.utc) return (now_date - change_date).days > self.credchange_required_days @property @@ -772,30 +771,23 @@ def credchange_recommended(self): change_date = self.credchange_date if change_date is None or self.credchange_recommended_days == -1: return False - now_date = self._datetime_to_utc(datetime.now()) + now_date = datetime.now(timezone.utc) return (now_date - change_date).days > self.credchange_recommended_days # ---------- Datetime Functions ---------- - def _datetime_to_utc(self, dt): - """Convert naive datetimes to UTC""" - - if not dt.tzinfo: - dt = dt.replace(tzinfo=tz.gettz()) - return dt.astimezone(tz.gettz('UTC')) - def _encode_time(self, value): """bytestring or plaintext string: Convert datetime to base64 or plaintext string""" if self.version >= (4, 0): diff_seconds = int( ( - self._datetime_to_utc(value) - + value - datetime( year=1, month=1, day=1, - tzinfo=tz.gettz('UTC') + tzinfo=timezone.utc ) ).total_seconds() ) @@ -803,7 +795,7 @@ def _encode_time(self, value): struct.pack('=2.7.0", "construct>=2.10.53", "argon2_cffi>=18.1.0", "pycryptodomex>=3.6.2", diff --git a/setup.py b/setup.py deleted file mode 100644 index b8d13a3c..00000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -from setuptools import find_packages, setup - -with open("README.rst") as file: - README = file.read() - -version = {} -with open("pykeepass/version.py") as file: - exec(file.read(), version) - -setup( - name="pykeepass", - version=version["__version__"], - license="GPL3", - description="Python library to interact with keepass databases " - "(supports KDBX3 and KDBX4)", - long_description=README, - long_description_content_type='text/x-rst', - author="Philipp Schmitt", - author_email="philipp@schmitt.co", - url="https://github.com/libkeepass/pykeepass", - packages=find_packages(include=['pykeepass', 'pykeepass.*']), - install_requires=[ - "python-dateutil", - "construct", - "argon2_cffi", - "pycryptodomex>=3.6.2", - "lxml", - ], - include_package_data=True, -) diff --git a/tests/tests.py b/tests/tests.py index 7ae2f98b..9f7e4daa 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -5,9 +5,8 @@ import shutil import unittest import uuid -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone -from dateutil import tz from pathlib import Path from io import BytesIO @@ -222,7 +221,7 @@ def test_history_group(self): def test_add_delete_move_entry(self): unique_str = 'test_add_entry_' - expiry_time = datetime.now() + expiry_time = datetime.now(timezone.utc) entry = self.kp.add_entry( self.kp.root_group, unique_str + 'title', @@ -246,8 +245,6 @@ def test_add_delete_move_entry(self): self.assertEqual(len(results.tags), 6) self.assertTrue(results.uuid != None) self.assertTrue(results.autotype_sequence is None) - # convert naive datetime to utc - expiry_time_utc = expiry_time.replace(tzinfo=tz.gettz()).astimezone(tz.gettz('UTC')) self.assertEqual(results.icon, icons.KEY) sub_group = self.kp.add_group(self.kp.root_group, 'sub_group') @@ -283,6 +280,52 @@ def test_raise_exception_entry(self): ) self.assertRaises(Exception, entry) + # ---------- Timezone test ----------- + + def test_expiration_time_tz(self): + # The expiration date is compared in UTC + # setting expiration date with tz offset 6 hours should result in expired entry + unique_str = 'test_exptime_tz_1_' + expiry_time = datetime.now(timezone(offset=timedelta(hours=6))).replace(microsecond=0) + self.kp.add_entry( + self.kp.root_group, + unique_str + 'title', + unique_str + 'user', + unique_str + 'pass', + expiry_time=expiry_time + ) + results = self.kp.find_entries_by_title(unique_str + 'title', first=True) + self.assertEqual(results.expired, True) + self.assertEqual(results.expiry_time, expiry_time.astimezone(timezone.utc)) + + # setting expiration date with UTC tz should result in expired entry + unique_str = 'test_exptime_tz_2_' + expiry_time = datetime.now(timezone.utc).replace(microsecond=0) + self.kp.add_entry( + self.kp.root_group, + unique_str + 'title', + unique_str + 'user', + unique_str + 'pass', + expiry_time=expiry_time + ) + results = self.kp.find_entries_by_title(unique_str + 'title', first=True) + self.assertEqual(results.expired, True) + self.assertEqual(results.expiry_time, expiry_time.astimezone(timezone.utc)) + + # setting expiration date with tz offset -6 hours while adding 6 hours should result in valid entry + unique_str = 'test_exptime_tz_3_' + expiry_time = datetime.now(timezone(offset=timedelta(hours=-6))).replace(microsecond=0) + timedelta(hours=6) + self.kp.add_entry( + self.kp.root_group, + unique_str + 'title', + unique_str + 'user', + unique_str + 'pass', + expiry_time=expiry_time + ) + results = self.kp.find_entries_by_title(unique_str + 'title', first=True) + self.assertEqual(results.expired, False) + self.assertEqual(results.expiry_time, expiry_time.astimezone(timezone.utc)) + # ---------- Entries representation ----------- def test_print_entries(self): @@ -433,7 +476,7 @@ def test_recyclebinemptying(self): class EntryTests3(KDBX3Tests): def test_fields(self): - time = datetime.now().replace(microsecond=0) + expiry_time = datetime.now(timezone.utc).replace(microsecond=0) entry = Entry( 'title', 'username', @@ -443,7 +486,7 @@ def test_fields(self): tags='tags', otp='otp', expires=True, - expiry_time=time, + expiry_time=expiry_time, icon=icons.KEY, kp=self.kp ) @@ -456,8 +499,7 @@ def test_fields(self): self.assertEqual(entry.tags, ['tags']) self.assertEqual(entry.otp, 'otp') self.assertEqual(entry.expires, True) - self.assertEqual(entry.expiry_time, - time.replace(tzinfo=tz.gettz()).astimezone(tz.gettz('UTC'))) + self.assertEqual(entry.expiry_time, expiry_time) self.assertEqual(entry.icon, icons.KEY) self.assertEqual(entry.is_a_history_entry, False) self.assertEqual( @@ -487,7 +529,7 @@ def test_references(self): self.assertNotEqual(clone1, clone2) def test_set_and_get_fields(self): - time = datetime.now().replace(microsecond=0) + time = datetime.now(timezone.utc).replace(microsecond=0) changed_time = time + timedelta(hours=9) changed_string = 'changed_' entry = Entry( @@ -528,8 +570,7 @@ def test_set_and_get_fields(self): self.assertEqual(entry.get_custom_property('foo'), None) # test time properties self.assertEqual(entry.expires, False) - self.assertEqual(entry.expiry_time, - changed_time.replace(tzinfo=tz.gettz()).astimezone(tz.gettz('UTC'))) + self.assertEqual(entry.expiry_time, changed_time) entry.tags = 'changed_tags' self.assertEqual(entry.tags, ['changed_tags']) @@ -540,8 +581,8 @@ def test_set_and_get_fields(self): def test_expired_datetime_offset(self): """Test for https://github.com/pschmitt/pykeepass/issues/115""" - future_time = datetime.now() + timedelta(days=1) - past_time = datetime.now() - timedelta(days=1) + future_time = datetime.now(timezone.utc) + timedelta(days=1) + past_time = datetime.now(timezone.utc) - timedelta(days=1) entry = Entry( 'title', 'username', @@ -695,7 +736,7 @@ def test_find_history_entries(self): # change the active entries to test integrity of the history items backup = {} - now = datetime.now() + now = datetime.now(timezone.utc) for entry in res1: backup[entry.uuid] = {"atime": entry.atime, "mtime": entry.mtime, "ctime": entry.ctime} entry.title = changed + 'title' @@ -863,8 +904,8 @@ def test_credchange(self): required_days = 5 recommended_days = 5 - unexpired_date = datetime.now() - timedelta(days=1) - expired_date = datetime.now() - timedelta(days=10) + unexpired_date = datetime.now(timezone.utc) - timedelta(days=1) + expired_date = datetime.now(timezone.utc) - timedelta(days=10) self.kp.credchange_required_days = required_days self.kp.credchange_recommended_days = recommended_days