Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use datetime functions from python standard lib #371

Merged
merged 8 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pykeepass/baseelement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
42 changes: 14 additions & 28 deletions pykeepass/pykeepass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -754,16 +753,16 @@ 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):
"""bool: Check if credential change is required"""
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
Expand All @@ -772,38 +771,31 @@ 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()
)
return base64.b64encode(
struct.pack('<Q', diff_seconds)
).decode('utf-8')
else:
return self._datetime_to_utc(value).isoformat()
return value.strftime(DT_ISOFORMAT)

def _decode_time(self, text):
"""datetime.datetime: Convert base64 time or plaintext time to datetime"""
Expand All @@ -812,21 +804,15 @@ def _decode_time(self, text):
# decode KDBX4 date from b64 format
try:
return (
datetime(year=1, month=1, day=1, tzinfo=tz.gettz('UTC')) +
datetime(year=1, month=1, day=1, tzinfo=timezone.utc) +
timedelta(
seconds=struct.unpack('<Q', base64.b64decode(text))[0]
)
)
except BinasciiError:
return parser.parse(
text,
tzinfos={'UTC': tz.gettz('UTC')}
)
return datetime.strptime(text, DT_ISOFORMAT).replace(tzinfo=timezone.utc)
else:
return parser.parse(
text,
tzinfos={'UTC': tz.gettz('UTC')}
)
return datetime.strptime(text, DT_ISOFORMAT).replace(tzinfo=timezone.utc)

def create_database(
filename, password=None, keyfile=None, transformed_key=None
Expand Down
6 changes: 4 additions & 2 deletions pykeepass/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__version__ = "4.0.6"

__all__= ["__version__"]

# FIXME: switch to using importlib.metadata when dropping Python<=3.7
import pkg_resources
__version__ = pkg_resources.get_distribution('pykeepass').version
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -9,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",
Expand Down
30 changes: 0 additions & 30 deletions setup.py

This file was deleted.

75 changes: 58 additions & 17 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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')
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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',
Expand All @@ -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
)
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'])
Expand All @@ -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',
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading