diff --git a/pykeepass/__init__.py b/pykeepass/__init__.py index c39548a4..72135b4c 100644 --- a/pykeepass/__init__.py +++ b/pykeepass/__init__.py @@ -1,6 +1,4 @@ -from __future__ import absolute_import 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 9833eb37..95ebf51f 100644 --- a/pykeepass/attachment.py +++ b/pykeepass/attachment.py @@ -1,7 +1,8 @@ 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 2ca238fa..7c6e950e 100644 --- a/pykeepass/baseelement.py +++ b/pykeepass/baseelement.py @@ -1,11 +1,13 @@ import base64 import uuid +from datetime import datetime, timezone + + from lxml import etree from lxml.builder import E -from datetime import datetime, timezone -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..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): @@ -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..8b24bcb9 100644 --- a/pykeepass/kdbx_parsing/common.py +++ b/pykeepass/kdbx_parsing/common.py @@ -1,21 +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 unicodedata -import zlib -import re -import codecs -from io import BytesIO -from collections import OrderedDict -import logging + +from .twofish import Twofish log = logging.getLogger(__name__) @@ -206,7 +217,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/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 08be860b..5ef69609 100644 --- a/pykeepass/pykeepass.py +++ b/pykeepass/pykeepass.py @@ -1,4 +1,3 @@ -# coding: utf-8 import base64 import logging import os @@ -7,17 +6,24 @@ import struct import uuid import zlib - from binascii import Error as BinasciiError -from construct import Container, ChecksumError, CheckError from datetime import datetime, timedelta, timezone +from pathlib import Path + +from construct import Container, ChecksumError, CheckError + from lxml import etree from lxml.builder import E -from pathlib import Path 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 @@ -30,7 +36,7 @@ BLANK_DATABASE_PASSWORD = "password" DT_ISOFORMAT = "%Y-%m-%dT%H:%M:%S%fZ" -class PyKeePass(): +class PyKeePass: """Open a KeePass database Args: @@ -374,24 +380,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) @@ -420,8 +426,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): @@ -505,9 +513,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): @@ -610,10 +623,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.payload.inner_header.binary.append(c) diff --git a/tests/tests.py b/tests/tests.py index 9f7e4daa..671381df 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,20 +1,16 @@ -# -*- coding: utf-8 -*- - import logging import os import shutil import unittest import uuid from datetime import datetime, timedelta, timezone - -from pathlib import Path - from io import BytesIO +from pathlib import Path 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: @@ -243,7 +239,7 @@ 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) self.assertEqual(results.icon, icons.KEY) @@ -398,7 +394,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)