From 381f48f6d0d2834c6784885e50b1cac85a0beaff Mon Sep 17 00:00:00 2001 From: evan Date: Fri, 9 Feb 2024 23:04:07 -0600 Subject: [PATCH 1/8] remote setup.py, get version.py info from pyproject.toml --- pykeepass/version.py | 3 ++- pyproject.toml | 1 + setup.py | 30 ------------------------------ 3 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 setup.py diff --git a/pykeepass/version.py b/pykeepass/version.py index 6ca1dadc..a0af0fdb 100644 --- a/pykeepass/version.py +++ b/pykeepass/version.py @@ -1,3 +1,4 @@ -__version__ = "4.0.6" +import importlib.metadata +__version__ = importlib.metadata.version('pykeepass') __all__= ["__version__"] diff --git a/pyproject.toml b/pyproject.toml index 33b4e51b..8650842c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [project] name = "pykeepass" +version = "4.0.7" readme = "README.rst" description = "Python library to interact with keepass databases (supports KDBX3 and KDBX4)" authors = [ 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, -) From a8aba4ff9d754610317d8c35a64b29571db57413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Wed, 3 Jan 2024 09:52:04 +0100 Subject: [PATCH 2/8] Use timezone class from stdlib --- pykeepass/baseelement.py | 10 +++++----- pykeepass/pykeepass.py | 32 +++++++++++++------------------- tests/tests.py | 29 ++++++++++++----------------- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/pykeepass/baseelement.py b/pykeepass/baseelement.py index 4a5230e6..6e4e96f0 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,7 +17,7 @@ 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) else: @@ -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..4a7fe134 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -5,13 +5,14 @@ import re import shutil import struct +import time import uuid import zlib from binascii import Error as BinasciiError from construct import Container, ChecksumError, CheckError -from dateutil import parser, tz -from datetime import datetime, timedelta +from dateutil import parser +from datetime import datetime, timedelta, timezone from lxml import etree from lxml.builder import E from pathlib import Path @@ -707,7 +708,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 +718,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): @@ -763,7 +764,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 +773,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 +797,7 @@ def _encode_time(self, value): struct.pack(' Date: Thu, 4 Jan 2024 09:13:32 +0100 Subject: [PATCH 3/8] Parse date values using fromisoformat --- pykeepass/pykeepass.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 4a7fe134..5db4682a 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -5,13 +5,11 @@ import re import shutil import struct -import time import uuid import zlib from binascii import Error as BinasciiError from construct import Container, ChecksumError, CheckError -from dateutil import parser from datetime import datetime, timedelta, timezone from lxml import etree from lxml.builder import E @@ -755,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): @@ -812,15 +810,9 @@ def _decode_time(self, text): ) ) except BinasciiError: - return parser.parse( - text, - tzinfos={'UTC': timezone.utc} - ) + return datetime.fromisoformat(text).astimezone(timezone.utc) else: - return parser.parse( - text, - tzinfos={'UTC': timezone.utc} - ) + return datetime.fromisoformat(text).astimezone(timezone.utc) def create_database( filename, password=None, keyfile=None, transformed_key=None From e5d87aefff000c729fff91219987f8ef12055e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Thu, 4 Jan 2024 09:16:05 +0100 Subject: [PATCH 4/8] Add timezone test for expiration date --- tests/tests.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index 4344a7db..9f7e4daa 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -280,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): From e78cf4d0281bf3a41a964ec4f1131e30afd1a04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Thu, 4 Jan 2024 13:55:41 +0100 Subject: [PATCH 5/8] Use date parse function for P3.6 compatibility --- pykeepass/pykeepass.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 5db4682a..f0131c1c 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -28,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%z" class PyKeePass(): """Open a KeePass database @@ -810,9 +810,9 @@ def _decode_time(self, text): ) ) except BinasciiError: - return datetime.fromisoformat(text).astimezone(timezone.utc) + return datetime.strptime(text, DT_ISOFORMAT).astimezone(timezone.utc) else: - return datetime.fromisoformat(text).astimezone(timezone.utc) + return datetime.strptime(text, DT_ISOFORMAT).astimezone(timezone.utc) def create_database( filename, password=None, keyfile=None, transformed_key=None From 4739f8d71f61ff6bf1510b91607d0161a57ef9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Sun, 7 Jan 2024 11:45:12 +0100 Subject: [PATCH 6/8] Update datetime format string WIP sad update format --- pykeepass/baseelement.py | 2 +- pykeepass/pykeepass.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pykeepass/baseelement.py b/pykeepass/baseelement.py index 6e4e96f0..2ca238fa 100644 --- a/pykeepass/baseelement.py +++ b/pykeepass/baseelement.py @@ -19,7 +19,7 @@ def __init__(self, element, kp=None, icon=None, expires=False, self._element.append(E.IconID(icon)) 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 diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index f0131c1c..93eb71e9 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -28,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%z" +DT_ISOFORMAT = "%Y-%m-%dT%H:%M:%S%fZ" class PyKeePass(): """Open a KeePass database @@ -795,7 +795,7 @@ def _encode_time(self, value): struct.pack(' Date: Fri, 9 Feb 2024 22:48:54 -0600 Subject: [PATCH 7/8] remove dateutil dep --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8650842c..61e103d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ authors = [ license = {text = "GPL-3.0"} keywords = ["vault", "keepass"] dependencies = [ - "python-dateutil>=2.7.0", "construct>=2.10.53", "argon2_cffi>=18.1.0", "pycryptodomex>=3.6.2", From 7611eadee6918345b82109c255b65e748711a7bd Mon Sep 17 00:00:00 2001 From: evan Date: Fri, 9 Feb 2024 23:46:22 -0600 Subject: [PATCH 8/8] backwards compatible __version__ --- pykeepass/version.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pykeepass/version.py b/pykeepass/version.py index a0af0fdb..c408a4e9 100644 --- a/pykeepass/version.py +++ b/pykeepass/version.py @@ -1,4 +1,5 @@ -import importlib.metadata - -__version__ = importlib.metadata.version('pykeepass') __all__= ["__version__"] + +# FIXME: switch to using importlib.metadata when dropping Python<=3.7 +import pkg_resources +__version__ = pkg_resources.get_distribution('pykeepass').version