From 0ac962e75649d6a758de2e20b92de045b5db56f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Tue, 9 Jan 2024 13:38:42 +0100 Subject: [PATCH 1/4] Use modern Python syntax These edits were suggested by Ruff linter. I've selected only the most obvious fixes to make it easier to review. --- pykeepass/__init__.py | 1 - pykeepass/attachment.py | 2 +- pykeepass/baseelement.py | 2 +- pykeepass/entry.py | 6 +++--- pykeepass/exceptions.py | 2 +- pykeepass/group.py | 4 ++-- pykeepass/kdbx_parsing/common.py | 3 +-- pykeepass/pykeepass.py | 18 +++++++----------- tests/tests.py | 7 ++----- 9 files changed, 18 insertions(+), 27 deletions(-) diff --git a/pykeepass/__init__.py b/pykeepass/__init__.py index c39548a4..4b97e7fb 100644 --- a/pykeepass/__init__.py +++ b/pykeepass/__init__.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import from .pykeepass import PyKeePass, create_database from .version import __version__ diff --git a/pykeepass/attachment.py b/pykeepass/attachment.py index 9833eb37..2599ddd8 100644 --- a/pykeepass/attachment.py +++ b/pykeepass/attachment.py @@ -1,7 +1,7 @@ from . import entry from .exceptions import BinaryError -class Attachment(object): +class Attachment: def __init__(self, element=None, kp=None, id=None, filename=None): self._element = element self._kp = kp diff --git a/pykeepass/baseelement.py b/pykeepass/baseelement.py index 4a5230e6..611aef07 100644 --- a/pykeepass/baseelement.py +++ b/pykeepass/baseelement.py @@ -5,7 +5,7 @@ from datetime import datetime -class BaseElement(): +class BaseElement: """Entry and Group inherit from this class""" def __init__(self, element, kp=None, icon=None, expires=False, diff --git a/pykeepass/entry.py b/pykeepass/entry.py index 2f6287ef..ec35e445 100644 --- a/pykeepass/entry.py +++ b/pykeepass/entry.py @@ -54,7 +54,7 @@ def __init__(self, title=None, username=None, password=None, url=None, ) if tags: self._element.append( - E.Tags(';'.join(tags) if type(tags) is list else tags) + E.Tags(';'.join(tags) if isinstance(tags, list) else tags) ) self._element.append( E.AutoType( @@ -221,7 +221,7 @@ def tags(self): @tags.setter def tags(self, value, sep=';'): # Accept both str or list - v = sep.join(value if type(value) is list else [value]) + v = sep.join(value if isinstance(value, list) else [value]) return self._set_subelement_text('Tags', v) @property @@ -403,7 +403,7 @@ def delete_history(self, history_entry=None, all=False): def __str__(self): # filter out NoneTypes and join into string - pathstr = '/'.join('' if p==None else p for p in self.path) + pathstr = '/'.join('' if p is None else p for p in self.path) return 'Entry: "{} ({})"'.format(pathstr, self.username) diff --git a/pykeepass/exceptions.py b/pykeepass/exceptions.py index 65d7a65a..d627c47c 100644 --- a/pykeepass/exceptions.py +++ b/pykeepass/exceptions.py @@ -21,4 +21,4 @@ class BinaryError(Exception): # ----- RecycleBin exceptions ----- class UnableToSendToRecycleBin(Exception): - pass \ No newline at end of file + pass diff --git a/pykeepass/group.py b/pykeepass/group.py index c5b21a0c..c22f74f5 100644 --- a/pykeepass/group.py +++ b/pykeepass/group.py @@ -93,7 +93,7 @@ def append(self, entries): Args: entries (:obj:`Entry` or :obj:`list` of :obj:`Entry`) """ - if type(entries) is list: + if isinstance(entries, list): for e in entries: self._element.append(e._element) else: @@ -101,5 +101,5 @@ def append(self, entries): def __str__(self): # filter out NoneTypes and join into string - pathstr = '/'.join('' if p==None else p for p in self.path) + pathstr = '/'.join('' if p is None else p for p in self.path) return 'Group: "{}"'.format(pathstr) diff --git a/pykeepass/kdbx_parsing/common.py b/pykeepass/kdbx_parsing/common.py index 2ef3d267..fa0ea074 100644 --- a/pykeepass/kdbx_parsing/common.py +++ b/pykeepass/kdbx_parsing/common.py @@ -9,7 +9,6 @@ from copy import deepcopy import base64 from binascii import Error as BinasciiError -import unicodedata import zlib import re import codecs @@ -206,7 +205,7 @@ def _decode(self, tree, con, path): result = cipher.decrypt(base64.b64decode(elem.text)).decode('utf-8') # strip invalid XML characters - https://stackoverflow.com/questions/8733233 result = re.sub( - u'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', + '[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', result ) diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 761828b4..23af0475 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -1,4 +1,3 @@ -# coding: utf-8 import base64 import logging import os @@ -18,7 +17,7 @@ from .attachment import Attachment from .entry import Entry -from .exceptions import * +from .exceptions import BinaryError, CredentialsError, HeaderChecksumError, PayloadChecksumError, UnableToSendToRecycleBin from .group import Group from .kdbx_parsing import KDBX, kdf_uuids from .xpath import attachment_xp, entry_xp, group_xp, path_xp @@ -31,7 +30,7 @@ BLANK_DATABASE_PASSWORD = "password" -class PyKeePass(): +class PyKeePass: """Open a KeePass database Args: @@ -366,24 +365,24 @@ def _find(self, prefix, keys_xp, path=None, tree=None, first=False, xp += prefix # handle searching custom string fields - if 'string' in kwargs.keys(): + if 'string' in kwargs: for key, value in kwargs['string'].items(): xp += keys_xp[regex]['string'].format(key, value, flags=flags) kwargs.pop('string') # convert uuid to base64 form before building xpath - if 'uuid' in kwargs.keys(): + if 'uuid' in kwargs: kwargs['uuid'] = base64.b64encode(kwargs['uuid'].bytes).decode('utf-8') # convert tags to semicolon separated string before building xpath # FIXME: this isn't a reliable way to search tags. e.g. searching ['tag1', 'tag2'] will match 'tag1tag2 - if 'tags' in kwargs.keys(): + if 'tags' in kwargs: kwargs['tags'] = ' and '.join(f'contains(text(),"{t}")' for t in kwargs['tags']) # build xpath to filter results with specified attributes for key, value in kwargs.items(): - if key not in keys_xp[regex].keys(): + if key not in keys_xp[regex]: raise TypeError('Invalid keyword argument "{}"'.format(key)) if value is not None: xp += keys_xp[regex][key].format(value, flags=flags) @@ -602,10 +601,7 @@ def binaries(self): def add_binary(self, data, compressed=True, protected=True): if self.version >= (4, 0): # add protected flag byte - if protected: - data = b'\x01' + data - else: - data = b'\x00' + data + data = b'\x01' + data if protected else b'\x00' + data # add binary element to inner header c = Container(type='binary', data=data) self.kdbx.body.payload.inner_header.binary.append(c) diff --git a/tests/tests.py b/tests/tests.py index 7ae2f98b..169f3957 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import logging import os import shutil @@ -244,10 +242,9 @@ def test_add_delete_move_entry(self): self.assertEqual(results.url, unique_str + 'url') self.assertEqual(results.notes, unique_str + 'notes') self.assertEqual(len(results.tags), 6) - self.assertTrue(results.uuid != None) + self.assertTrue(results.uuid is not 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') @@ -355,7 +352,7 @@ def test_add_delete_move_group(self): results = self.kp.find_groups(path=['base_group', 'sub_group'], first=True) self.assertIsInstance(results, Group) self.assertEqual(results.name, sub_group.name) - self.assertTrue(results.uuid != None) + self.assertTrue(results.uuid is not None) self.kp.move_group(sub_group2, sub_group) results = self.kp.find_groups(path=['base_group', 'sub_group', 'sub_group2'], first=True) From 2d1194d437ef4a81266937bf5cb3f9ab156d481b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kroupa?= Date: Tue, 9 Jan 2024 13:46:19 +0100 Subject: [PATCH 2/4] Sort imports Imports are sorted using default ruff linter configuration. --- pykeepass/__init__.py | 1 - pykeepass/attachment.py | 1 + pykeepass/baseelement.py | 3 +- pykeepass/group.py | 2 +- pykeepass/kdbx_parsing/common.py | 38 +++++++++++++-------- pykeepass/kdbx_parsing/kdbx.py | 8 +++-- pykeepass/kdbx_parsing/kdbx3.py | 44 +++++++++++++++++++++---- pykeepass/kdbx_parsing/kdbx4.py | 51 ++++++++++++++++++++++++----- pykeepass/kdbx_parsing/pytwofish.py | 1 + pykeepass/kdbx_parsing/twofish.py | 5 +-- pykeepass/pykeepass.py | 33 +++++++++++++------ tests/tests.py | 7 ++-- 12 files changed, 143 insertions(+), 51 deletions(-) diff --git a/pykeepass/__init__.py b/pykeepass/__init__.py index 4b97e7fb..72135b4c 100644 --- a/pykeepass/__init__.py +++ b/pykeepass/__init__.py @@ -1,5 +1,4 @@ from .pykeepass import PyKeePass, create_database - from .version import __version__ __all__ = ["PyKeePass", "create_database", "__version__"] diff --git a/pykeepass/attachment.py b/pykeepass/attachment.py index 2599ddd8..95ebf51f 100644 --- a/pykeepass/attachment.py +++ b/pykeepass/attachment.py @@ -1,6 +1,7 @@ from . import entry from .exceptions import BinaryError + class Attachment: def __init__(self, element=None, kp=None, id=None, filename=None): self._element = element diff --git a/pykeepass/baseelement.py b/pykeepass/baseelement.py index 611aef07..ec870eff 100644 --- a/pykeepass/baseelement.py +++ b/pykeepass/baseelement.py @@ -1,8 +1,9 @@ import base64 import uuid +from datetime import datetime + from lxml import etree from lxml.builder import E -from datetime import datetime class BaseElement: diff --git a/pykeepass/group.py b/pykeepass/group.py index c22f74f5..2ce16b71 100644 --- a/pykeepass/group.py +++ b/pykeepass/group.py @@ -2,8 +2,8 @@ from lxml.etree import Element, _Element from lxml.objectify import ObjectifiedElement -from .entry import Entry from .baseelement import BaseElement +from .entry import Entry class Group(BaseElement): diff --git a/pykeepass/kdbx_parsing/common.py b/pykeepass/kdbx_parsing/common.py index fa0ea074..8b24bcb9 100644 --- a/pykeepass/kdbx_parsing/common.py +++ b/pykeepass/kdbx_parsing/common.py @@ -1,20 +1,32 @@ -from Cryptodome.Cipher import AES, ChaCha20, Salsa20 -from .twofish import Twofish -from Cryptodome.Util import Padding as CryptoPadding +import base64 +import codecs import hashlib +import logging +import re +import zlib +from binascii import Error as BinasciiError +from collections import OrderedDict +from copy import deepcopy +from io import BytesIO + from construct import ( - Adapter, BitStruct, BitsSwapped, Container, Flag, Padding, ListContainer, Mapping, GreedyBytes, Int32ul, Switch + Adapter, + BitsSwapped, + BitStruct, + Container, + Flag, + GreedyBytes, + Int32ul, + ListContainer, + Mapping, + Padding, + Switch, ) +from Cryptodome.Cipher import AES, ChaCha20, Salsa20 +from Cryptodome.Util import Padding as CryptoPadding from lxml import etree -from copy import deepcopy -import base64 -from binascii import Error as BinasciiError -import zlib -import re -import codecs -from io import BytesIO -from collections import OrderedDict -import logging + +from .twofish import Twofish log = logging.getLogger(__name__) diff --git a/pykeepass/kdbx_parsing/kdbx.py b/pykeepass/kdbx_parsing/kdbx.py index 3a43f7cf..935da4ae 100644 --- a/pykeepass/kdbx_parsing/kdbx.py +++ b/pykeepass/kdbx_parsing/kdbx.py @@ -1,8 +1,10 @@ -from construct import Struct, Switch, Bytes, Int16ul, RawCopy, Check, this -from .kdbx3 import DynamicHeader as DynamicHeader3 +from construct import Bytes, Check, Int16ul, RawCopy, Struct, Switch, this + from .kdbx3 import Body as Body3 -from .kdbx4 import DynamicHeader as DynamicHeader4 +from .kdbx3 import DynamicHeader as DynamicHeader3 from .kdbx4 import Body as Body4 +from .kdbx4 import DynamicHeader as DynamicHeader4 + # verify file signature def check_signature(ctx): diff --git a/pykeepass/kdbx_parsing/kdbx3.py b/pykeepass/kdbx_parsing/kdbx3.py index 87db0f31..16ef911c 100644 --- a/pykeepass/kdbx_parsing/kdbx3.py +++ b/pykeepass/kdbx_parsing/kdbx3.py @@ -2,18 +2,48 @@ # keepass decrypt experimentation import hashlib + from construct import ( - Byte, Bytes, Int16ul, Int32ul, Int64ul, RepeatUntil, GreedyBytes, Struct, - this, Mapping, Switch, Prefixed, Padding, Checksum, Computed, IfThenElse, - Pointer, Tell, len_, If + Byte, + Bytes, + Checksum, + Computed, + GreedyBytes, + If, + IfThenElse, + Int16ul, + Int32ul, + Int64ul, + Mapping, + Padding, + Pointer, + Prefixed, + RepeatUntil, + Struct, + Switch, + Tell, + len_, + this, ) + from .common import ( - aes_kdf, AES256Payload, ChaCha20Payload, TwoFishPayload, Concatenated, - DynamicDict, compute_key_composite, Decompressed, Reparsed, - compute_master, CompressionFlags, XML, CipherId, ProtectedStreamId, Unprotect + XML, + AES256Payload, + ChaCha20Payload, + CipherId, + CompressionFlags, + Concatenated, + Decompressed, + DynamicDict, + ProtectedStreamId, + Reparsed, + TwoFishPayload, + Unprotect, + aes_kdf, + compute_key_composite, + compute_master, ) - # -------------------- Key Derivation -------------------- # https://github.com/keepassxreboot/keepassxc/blob/8324d03f0a015e62b6182843b4478226a5197090/src/format/KeePass2.cpp#L24-L26 diff --git a/pykeepass/kdbx_parsing/kdbx4.py b/pykeepass/kdbx_parsing/kdbx4.py index 41ce7a89..426d3ac7 100644 --- a/pykeepass/kdbx_parsing/kdbx4.py +++ b/pykeepass/kdbx_parsing/kdbx4.py @@ -1,22 +1,55 @@ # Evan Widloski - 2018-04-11 # keepass decrypt experimentation -import struct import hashlib -import argon2 import hmac +import struct + +import argon2 from construct import ( - Byte, Bytes, Int32ul, RepeatUntil, GreedyBytes, Struct, this, Mapping, - Switch, Flag, Prefixed, Int64ul, Int32sl, Int64sl, GreedyString, Padding, - Peek, Checksum, Computed, IfThenElse, Pointer, Tell, If + Byte, + Bytes, + Checksum, + Computed, + Flag, + GreedyBytes, + GreedyString, + If, + IfThenElse, + Int32sl, + Int32ul, + Int64sl, + Int64ul, + Mapping, + Padding, + Peek, + Pointer, + Prefixed, + RepeatUntil, + Struct, + Switch, + Tell, + this, ) + from .common import ( - aes_kdf, Concatenated, AES256Payload, ChaCha20Payload, TwoFishPayload, - DynamicDict, compute_key_composite, Reparsed, Decompressed, - compute_master, CompressionFlags, XML, CipherId, ProtectedStreamId, Unprotect + XML, + AES256Payload, + ChaCha20Payload, + CipherId, + CompressionFlags, + Concatenated, + Decompressed, + DynamicDict, + ProtectedStreamId, + Reparsed, + TwoFishPayload, + Unprotect, + aes_kdf, + compute_key_composite, + compute_master, ) - # -------------------- Key Derivation -------------------- # https://github.com/keepassxreboot/keepassxc/blob/bc55974ff304794e53c925442784c50a2fdaf6ee/src/format/KeePass2.cpp#L30-L33 diff --git a/pykeepass/kdbx_parsing/pytwofish.py b/pykeepass/kdbx_parsing/pytwofish.py index 44671867..92553771 100644 --- a/pykeepass/kdbx_parsing/pytwofish.py +++ b/pykeepass/kdbx_parsing/pytwofish.py @@ -138,6 +138,7 @@ def get_key_size(self): import struct + def rotr32(x, n): return (x >> n) | ((x << (32 - n)) & 0xFFFFFFFF) diff --git a/pykeepass/kdbx_parsing/twofish.py b/pykeepass/kdbx_parsing/twofish.py index c4dc7a7e..99db21a5 100644 --- a/pykeepass/kdbx_parsing/twofish.py +++ b/pykeepass/kdbx_parsing/twofish.py @@ -24,9 +24,10 @@ __all__ = ['Twofish'] -from . import pytwofish -from Cryptodome.Util.strxor import strxor from Cryptodome.Util.Padding import pad +from Cryptodome.Util.strxor import strxor + +from . import pytwofish MODE_ECB = 1 MODE_CBC = 2 diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 23af0475..61a4fb22 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -6,18 +6,24 @@ import struct 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 pathlib import Path + +from construct import CheckError, ChecksumError, Container +from dateutil import parser, tz from lxml import etree from lxml.builder import E -from pathlib import Path from .attachment import Attachment from .entry import Entry -from .exceptions import BinaryError, CredentialsError, HeaderChecksumError, PayloadChecksumError, UnableToSendToRecycleBin +from .exceptions import ( + BinaryError, + CredentialsError, + HeaderChecksumError, + PayloadChecksumError, + UnableToSendToRecycleBin, +) from .group import Group from .kdbx_parsing import KDBX, kdf_uuids from .xpath import attachment_xp, entry_xp, group_xp, path_xp @@ -411,8 +417,10 @@ def _can_be_moved_to_recyclebin(self, entry_or_group): # ---------- Groups ---------- from .deprecated import ( - find_groups_by_name, find_groups_by_path, find_groups_by_uuid, - find_groups_by_notes + find_groups_by_name, + find_groups_by_notes, + find_groups_by_path, + find_groups_by_uuid, ) def find_groups(self, recursive=True, path=None, group=None, **kwargs): @@ -496,9 +504,14 @@ def empty_group(self, group): from .deprecated import ( - find_entries_by_title, find_entries_by_username, find_entries_by_password, - find_entries_by_url, find_entries_by_path, find_entries_by_notes, - find_entries_by_string, find_entries_by_uuid + find_entries_by_notes, + find_entries_by_password, + find_entries_by_path, + find_entries_by_string, + find_entries_by_title, + find_entries_by_url, + find_entries_by_username, + find_entries_by_uuid, ) def find_entries(self, recursive=True, path=None, group=None, **kwargs): diff --git a/tests/tests.py b/tests/tests.py index 169f3957..595f0a08 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -4,16 +4,15 @@ import unittest import uuid from datetime import datetime, timedelta - -from dateutil import tz +from io import BytesIO from pathlib import Path -from io import BytesIO +from dateutil import tz from pykeepass import PyKeePass, icons from pykeepass.entry import Entry -from pykeepass.group import Group from pykeepass.exceptions import BinaryError, CredentialsError, HeaderChecksumError +from pykeepass.group import Group """ Missing Tests: From 9c1835dec583e63a6a7e8dec2539051457ba9969 Mon Sep 17 00:00:00 2001 From: ekuler Date: Thu, 15 Feb 2024 10:14:13 +0100 Subject: [PATCH 3/4] Fix merge issue --- tests/tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index ed1ae0c5..671381df 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -7,8 +7,6 @@ from io import BytesIO from pathlib import Path -from dateutil import tz - from pykeepass import PyKeePass, icons from pykeepass.entry import Entry from pykeepass.exceptions import BinaryError, CredentialsError, HeaderChecksumError From 747603ee18cae185d09487738c6c7013e4b36c7b Mon Sep 17 00:00:00 2001 From: ekuler Date: Thu, 15 Feb 2024 10:14:33 +0100 Subject: [PATCH 4/4] Fix merge issue in pykeepass.py --- pykeepass/pykeepass.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pykeepass/pykeepass.py b/pykeepass/pykeepass.py index 0b1832ea..5ef69609 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -12,7 +12,6 @@ from construct import Container, ChecksumError, CheckError -from dateutil import parser, tz from lxml import etree from lxml.builder import E