pykeepass

pykeepass

This library allows you to write entries to a KeePass database.

Come chat at #pykeepass:matrix.org on Matrix.

Installation

sudo apt install python3-lxml
pip install pykeepass

Quickstart

General database manipulation

from pykeepass import PyKeePass

# load database
>>> kp = PyKeePass('db.kdbx', password='somePassw0rd')

# get all entries
>>> kp.entries
[Entry: "foo_entry (myusername)", Entry: "foobar_entry (myusername)", ...]

# find any group by its name
>>> group = kp.find_groups(name='social', first=True)

# get the entries in a group
>>> group.entries
[Entry: "social/facebook (myusername)", Entry: "social/twitter (myusername)"]

# find any entry by its title
>>> entry = kp.find_entries(title='facebook', first=True)

# retrieve the associated password and OTP information
>>> entry.password
's3cure_p455w0rd'
>>> entry.otp
otpauth://totp/test:lkj?secret=TEST%3D%3D%3D%3D&period=30&digits=6&issuer=test

# update an entry
>>> entry.notes = 'primary facebook account'

# create a new group
>>> group = kp.add_group(kp.root_group, 'email')

# create a new entry
>>> kp.add_entry(group, 'gmail', 'myusername', 'myPassw0rdXX')
Entry: "email/gmail (myusername)"

# save database
>>> kp.save()

Finding and manipulating entries

# add a new entry to the Root group
>>> kp.add_entry(kp.root_group, 'testing', 'foo_user', 'passw0rd')
Entry: "testing (foo_user)"

# add a new entry to the social group
>>> group = kp.find_groups(name='social', first=True)
>>> entry = kp.add_entry(group, 'testing', 'foo_user', 'passw0rd')
Entry: "testing (foo_user)"

# save the database
>>> kp.save()

# delete an entry
>>> kp.delete_entry(entry)

# move an entry
>>> kp.move_entry(entry, kp.root_group)

# save the database
>>> kp.save()

# change creation time
>>> from datetime import datetime, timezone
>>> entry.ctime = datetime(2023, 1, 1, tzinfo=timezone.utc)

# update modification or access time
>>> entry.touch(modify=True)

# save entry history
>>> entry.save_history()

Finding and manipulating groups

>>> kp.groups
[Group: "foo", Group "foobar", Group: "social", Group: "social/foo_subgroup"]

>>> kp.find_groups(name='foo', first=True)
Group: "foo"

>>> kp.find_groups(name='foo.*', regex=True)
[Group: "foo", Group "foobar"]

>>> kp.find_groups(path=['social'], regex=True)
[Group: "social", Group: "social/foo_subgroup"]

>>> kp.find_groups(name='social', first=True).subgroups
[Group: "social/foo_subgroup"]

>>> kp.root_group
Group: "/"

# add a new group to the Root group
>>> group = kp.add_group(kp.root_group, 'social')

# add a new group to the social group
>>> group2 = kp.add_group(group, 'gmail')
Group: "social/gmail"

# save the database
>>> kp.save()

# delete a group
>>> kp.delete_group(group)

# move a group
>>> kp.move_group(group2, kp.root_group)

# save the database
>>> kp.save()

# change creation time
>>> from datetime import datetime, timezone
>>> group.ctime = datetime(2023, 1, 1, tzinfo=timezone.utc)

# update modification or access time
>>> group.touch(modify=True)

Attachments

>>> e = kp.add_entry(kp.root_group, title='foo', username='', password='')

# add attachment data to the db
>>> binary_id = kp.add_binary(b'Hello world')

>>> kp.binaries
[b'Hello world']

# add attachment reference to entry
>>> a = e.add_attachment(binary_id, 'hello.txt')
>>> a
Attachment: 'hello.txt' -> 0

# access attachments
>>> a
Attachment: 'hello.txt' -> 0
>>> a.id
0
>>> a.filename
'hello.txt'
>>> a.data
b'Hello world'
>>> e.attachments
[Attachment: 'hello.txt' -> 0]

# list all attachments in the database
>>> kp.attachments
[Attachment: 'hello.txt' -> 0]

# search attachments
>>> kp.find_attachments(filename='hello.txt')
[Attachment: 'hello.txt** -> 0]

# delete attachment reference
>>> e.delete_attachment(a)

# or, delete both attachment reference and binary
>>> kp.delete_binary(binary_id**

OTP codes

# find an entry which has otp attribute
>>> e = kp.find_entries(otp='.*', regex=True, first=True)
>>> import pyotp
>>> pyotp.parse_uri(e.otp).now()
799270

Tests and Debugging

Run tests with python tests/tests.py or python tests/tests.py SomeSpecificTest

Enable debugging when doing tests in console:

>>> from pykeepass.pykeepass import debug_setup
>>> debug_setup()
>>> kp.entries[0]
DEBUG:pykeepass.pykeepass:xpath query: //Entry
DEBUG:pykeepass.pykeepass:xpath query: (ancestor::Group)[last()]
DEBUG:pykeepass.pykeepass:xpath query: (ancestor::Group)[last()]
DEBUG:pykeepass.pykeepass:xpath query: String/Key[text()="Title"]/../Value
DEBUG:pykeepass.pykeepass:xpath query: String/Key[text()="UserName"]/../Value
Entry: "root_entry (foobar_user)"
 1"""
 2.. include:: ../README.md
 3"""
 4
 5from .pykeepass import PyKeePass, create_database
 6from .entry import Entry
 7from .group import Group
 8from .attachment import Attachment
 9from .icons import icons
10from .version import __version__
11
12__all__ = ["__version__", "PyKeePass", "Entry", "Group", "Attachment", "icons", "create_database"]
__version__ = '4.1.0.post1'
class PyKeePass:
  38class PyKeePass:
  39    """Open a KeePass database
  40
  41    Args:
  42        filename (`str`, optional): path to database or stream object.
  43            If None, the path given when the database was opened is used.
  44        password (`str`, optional): database password.  If None,
  45            database is assumed to have no password
  46        keyfile (`str`, optional): path to keyfile.  If None,
  47            database is assumed to have no keyfile
  48        transformed_key (`bytes`, optional): precomputed transformed
  49            key.
  50        decrypt (`bool`, optional): whether to decrypt XML payload.
  51            Set `False` to access outer header information without decrypting
  52            database.
  53
  54    Raises:
  55        `CredentialsError`: raised when password/keyfile or transformed key
  56            are wrong
  57        `HeaderChecksumError`: raised when checksum in database header is
  58            is wrong.  e.g. database tampering or file corruption
  59        `PayloadChecksumError`: raised when payload blocks checksum is wrong,
  60            e.g. corruption during database saving
  61
  62    """
  63
  64    # TODO: raise, no filename provided, database not open
  65
  66    def __init__(self, filename, password=None, keyfile=None,
  67                 transformed_key=None, decrypt=True):
  68
  69        self.read(
  70            filename=filename,
  71            password=password,
  72            keyfile=keyfile,
  73            transformed_key=transformed_key,
  74            decrypt=decrypt
  75        )
  76
  77    def __enter__(self):
  78        return self
  79
  80    def __exit__(self, typ, value, tb):
  81        # see issue 137
  82        pass
  83
  84    def read(self, filename=None, password=None, keyfile=None,
  85             transformed_key=None, decrypt=True):
  86        """
  87        See class docstring.
  88        """
  89
  90        # TODO: - raise, no filename provided, database not open
  91        self._password = password
  92        self._keyfile = keyfile
  93        if filename:
  94            self.filename = filename
  95        else:
  96            filename = self.filename
  97
  98        try:
  99            if hasattr(filename, "read"):
 100                self.kdbx = KDBX.parse_stream(
 101                    filename,
 102                    password=password,
 103                    keyfile=keyfile,
 104                    transformed_key=transformed_key,
 105                    decrypt=decrypt
 106                )
 107            else:
 108                self.kdbx = KDBX.parse_file(
 109                    filename,
 110                    password=password,
 111                    keyfile=keyfile,
 112                    transformed_key=transformed_key,
 113                    decrypt=decrypt
 114                )
 115
 116        except CheckError as e:
 117            if e.path == '(parsing) -> header -> sig_check':
 118                raise HeaderChecksumError("Not a KeePass database")
 119            else:
 120                raise
 121
 122        # body integrity/verification
 123        except ChecksumError as e:
 124            if e.path in (
 125                    '(parsing) -> body -> cred_check', # KDBX4
 126                    '(parsing) -> cred_check' # KDBX3
 127                    ):
 128                raise CredentialsError("Invalid credentials")
 129            elif e.path == '(parsing) -> body -> sha256':
 130                raise HeaderChecksumError("Corrupted database")
 131            elif e.path in (
 132                    '(parsing) -> body -> payload -> hmac_hash', # KDBX4
 133                    '(parsing) -> xml -> block_hash' # KDBX3
 134                    ):
 135                raise PayloadChecksumError("Error reading database contents")
 136            else:
 137                raise
 138
 139    def reload(self):
 140        """Reload current database using previously given credentials """
 141
 142        self.read(self.filename, self.password, self.keyfile)
 143
 144    def save(self, filename=None, transformed_key=None):
 145        """Save current database object to disk.
 146
 147        Args:
 148            filename (`str`, optional): path to database or stream object.
 149                If None, the path given when the database was opened is used.
 150                PyKeePass.filename is unchanged.
 151            transformed_key (`bytes`, optional): precomputed transformed
 152                key.
 153        """
 154
 155        if not filename:
 156            filename = self.filename
 157
 158        if hasattr(filename, "write"):
 159            KDBX.build_stream(
 160                self.kdbx,
 161                filename,
 162                password=self.password,
 163                keyfile=self.keyfile,
 164                transformed_key=transformed_key,
 165                decrypt=True
 166            )
 167        else:
 168            # save to temporary file to prevent database clobbering
 169            # see issues 223, 101
 170            filename_tmp = Path(filename).with_suffix('.tmp')
 171            try:
 172                KDBX.build_file(
 173                    self.kdbx,
 174                    filename_tmp,
 175                    password=self.password,
 176                    keyfile=self.keyfile,
 177                    transformed_key=transformed_key,
 178                    decrypt=True
 179                )
 180            except Exception as e:
 181                os.remove(filename_tmp)
 182                raise e
 183            shutil.move(filename_tmp, filename)
 184
 185    @property
 186    def version(self):
 187        """`tuple` of `int`: Length 2 tuple of ints containing major and minor versions.
 188        Generally (3, 1) or (4, 0)."""
 189        return (
 190            self.kdbx.header.value.major_version,
 191            self.kdbx.header.value.minor_version
 192        )
 193
 194    @property
 195    def encryption_algorithm(self):
 196        """`str`: encryption algorithm used by database during decryption.
 197        Can be one of 'aes256', 'chacha20', or 'twofish'."""
 198        return self.kdbx.header.value.dynamic_header.cipher_id.data
 199
 200    @property
 201    def kdf_algorithm(self):
 202        """`str`: key derivation algorithm used by database during decryption.
 203        Can be one of 'aeskdf', 'argon2', or 'aeskdf'"""
 204        if self.version == (3, 1):
 205            return 'aeskdf'
 206        elif self.version == (4, 0):
 207            kdf_parameters = self.kdbx.header.value.dynamic_header.kdf_parameters.data.dict
 208            if kdf_parameters['$UUID'].value == kdf_uuids['argon2']:
 209                return 'argon2'
 210            elif kdf_parameters['$UUID'].value == kdf_uuids['argon2id']:
 211                return 'argon2id'
 212            elif kdf_parameters['$UUID'].value == kdf_uuids['aeskdf']:
 213                return 'aeskdf'
 214
 215    @property
 216    def transformed_key(self):
 217        """`bytes`: transformed key used in database decryption.  May be cached
 218        and passed to `open` for faster database opening"""
 219        return self.kdbx.body.transformed_key
 220
 221    @property
 222    def database_salt(self):
 223       """`bytes`: salt of database kdf. This can be used for adding additional
 224       credentials which are used in extension to current keyfile."""
 225
 226       if self.version == (3, 1):
 227            return self.kdbx.header.value.dynamic_header.transform_seed.data
 228
 229       kdf_parameters = self.kdbx.header.value.dynamic_header.kdf_parameters.data.dict
 230       return kdf_parameters['S'].value
 231
 232    @property
 233    def payload(self):
 234        """`construct.Container`: Encrypted payload of keepass database"""
 235
 236        # check if payload is decrypted
 237        if self.kdbx.body.payload is None:
 238            raise ValueError("Database is not decrypted")
 239        else:
 240            return self.kdbx.body.payload
 241
 242    @property
 243    def tree(self):
 244        """`lxml.etree._ElementTree`: database XML payload"""
 245        return self.payload.xml
 246
 247    @property
 248    def root_group(self):
 249        """`Group`: root Group of database"""
 250        return self.find_groups(path='', first=True)
 251
 252    @property
 253    def recyclebin_group(self):
 254        """`Group`: RecycleBin Group of database"""
 255        elem = self._xpath('/KeePassFile/Meta/RecycleBinUUID', first=True)
 256        recyclebin_uuid = uuid.UUID( bytes = base64.b64decode(elem.text) )
 257        return self.find_groups(uuid=recyclebin_uuid, first=True)
 258
 259    @property
 260    def groups(self):
 261        """`list` of `Group`: all groups in database
 262        """
 263        return self.find_groups()
 264
 265    @property
 266    def entries(self):
 267        """`list` of `Entry`: all entries in database,
 268        excluding history"""
 269        return self.find_entries()
 270
 271    @property
 272    def database_name(self):
 273        """`str`: Name of database"""
 274        elem = self._xpath('/KeePassFile/Meta/DatabaseName', first=True)
 275        return elem.text
 276
 277    @database_name.setter
 278    def database_name(self, name):
 279        item = self._xpath('/KeePassFile/Meta/DatabaseName', first=True)
 280        item.text = str(name)
 281
 282    @property
 283    def database_description(self):
 284        """`str`: Description of the database"""
 285        elem = self._xpath('/KeePassFile/Meta/DatabaseDescription', first=True)
 286        return elem.text
 287
 288    @database_description.setter
 289    def database_description(self, name):
 290        item = self._xpath('/KeePassFile/Meta/DatabaseDescription', first=True)
 291        item.text = str(name)
 292
 293    @property
 294    def default_username(self):
 295        """`str` or `None`: default user.  `None` if not set"""
 296        elem = self._xpath('/KeePassFile/Meta/DefaultUserName', first=True)
 297        return elem.text
 298
 299    @default_username.setter
 300    def default_username(self, name):
 301        item = self._xpath('/KeePassFile/Meta/DefaultUserName', first=True)
 302        item.text = str(name)
 303
 304    def xml(self):
 305        """Get XML part of database as string
 306
 307        Returns:
 308            `str`: XML content of database
 309        """
 310        return etree.tostring(
 311            self.tree,
 312            pretty_print=True,
 313            standalone=True,
 314            encoding='utf-8'
 315        )
 316
 317    def dump_xml(self, filename):
 318        """ Dump the contents of the database to file as XML
 319
 320        Args:
 321            filename (`str`): path to output file
 322        """
 323        with open(filename, 'wb') as f:
 324            f.write(self.xml())
 325
 326    def xpath(self, xpath_str, tree=None, first=False, cast=False, **kwargs):
 327        """Look up elements in the XML payload and return corresponding object.
 328
 329        Internal function which searches the payload lxml ElementTree for
 330        elements via XPath.  Matched entry, group, and attachment elements are
 331        automatically cast to their corresponding objects, otherwise an error
 332        is raised.
 333
 334        Args:
 335            xpath_str (`str`): XPath query for finding element(s)
 336            tree (`_ElementTree`, `Element`, optional): use this
 337                element as root node when searching
 338            first (`bool`): If True, function returns first result or None.  If
 339                False, function returns list of matches or empty list.
 340                    (default `False`).
 341            cast (`bool`): If True, matches are instead instantiated as
 342                pykeepass Group, Entry, or Attachment objects.  An exception
 343                is raised if a match cannot be cast.  (default `False`)
 344
 345        Returns:
 346            `list` of `Group`, `Entry`, `Attachment`, or `lxml.etree.Element`
 347        """
 348
 349        if tree is None:
 350            tree = self.tree
 351        logger.debug('xpath query: ' + xpath_str)
 352        elements = tree.xpath(
 353            xpath_str, namespaces={'re': 'http://exslt.org/regular-expressions'}
 354        )
 355
 356        res = []
 357        for e in elements:
 358            if cast:
 359                if e.tag == 'Entry':
 360                    res.append(Entry(element=e, kp=self))
 361                elif e.tag == 'Group':
 362                    res.append(Group(element=e, kp=self))
 363                elif e.tag == 'Binary' and e.getparent().tag == 'Entry':
 364                    res.append(Attachment(element=e, kp=self))
 365                else:
 366                    raise Exception('Could not cast element {}'.format(e))
 367            else:
 368                res.append(e)
 369
 370        # return first object in list or None
 371        if first:
 372            res = res[0] if res else None
 373
 374        return res
 375
 376    _xpath = xpath
 377
 378    def _find(self, prefix, keys_xp, path=None, tree=None, first=False,
 379              history=False, regex=False, flags=None, **kwargs):
 380        """Internal function for converting a search into an XPath string"""
 381
 382        xp = ''
 383
 384        if not history:
 385            prefix += '[not(ancestor::History)]'
 386
 387        if path is not None:
 388
 389            first = True
 390
 391            xp += '/KeePassFile/Root/Group'
 392            # split provided path into group and element
 393            group_path = path[:-1]
 394            element = path[-1] if len(path) > 0 else ''
 395            # build xpath from group_path and element
 396            for group in group_path:
 397                xp += path_xp[regex]['group'].format(group, flags=flags)
 398            if 'Entry' in prefix:
 399                xp += path_xp[regex]['entry'].format(element, flags=flags)
 400            elif element and 'Group' in prefix:
 401                xp += path_xp[regex]['group'].format(element, flags=flags)
 402
 403        else:
 404            if tree is not None:
 405                xp += '.'
 406
 407            xp += prefix
 408
 409            # handle searching custom string fields
 410            if 'string' in kwargs:
 411                for key, value in kwargs['string'].items():
 412                    xp += keys_xp[regex]['string'].format(key, value, flags=flags)
 413
 414                kwargs.pop('string')
 415
 416            # convert uuid to base64 form before building xpath
 417            if 'uuid' in kwargs:
 418                kwargs['uuid'] = base64.b64encode(kwargs['uuid'].bytes).decode('utf-8')
 419
 420            # convert tags to semicolon separated string before building xpath
 421            # FIXME: this isn't a reliable way to search tags.  e.g. searching ['tag1', 'tag2'] will match 'tag1tag2
 422            if 'tags' in kwargs:
 423                kwargs['tags'] = ' and '.join(f'contains(text(),"{t}")' for t in kwargs['tags'])
 424
 425            # build xpath to filter results with specified attributes
 426            for key, value in kwargs.items():
 427                if key not in keys_xp[regex]:
 428                    raise TypeError('Invalid keyword argument "{}"'.format(key))
 429                if value is not None:
 430                    xp += keys_xp[regex][key].format(value, flags=flags)
 431
 432        res = self._xpath(
 433            xp,
 434            tree=tree._element if tree else None,
 435            first=first,
 436            cast=True,
 437            **kwargs
 438        )
 439
 440        return res
 441
 442    def _can_be_moved_to_recyclebin(self, entry_or_group):
 443        if entry_or_group == self.root_group:
 444            return False
 445        recyclebin_group = self.recyclebin_group
 446        if recyclebin_group is None:
 447            return True
 448        uuid_str = base64.b64encode( entry_or_group.uuid.bytes).decode('utf-8')
 449        elem = self._xpath('./UUID[text()="{}"]/..'.format(uuid_str), tree=recyclebin_group._element, first=True, cast=False)
 450        return elem is None
 451
 452
 453    # ---------- Groups ----------
 454
 455    from .deprecated import (
 456        find_groups_by_name,
 457        find_groups_by_notes,
 458        find_groups_by_path,
 459        find_groups_by_uuid,
 460    )
 461
 462    def find_groups(self, recursive=True, path=None, group=None, **kwargs):
 463        """ Find groups in a database
 464
 465        [XSLT style]: https://www.xml.com/pub/a/2003/06/04/tr.html
 466        [flags]: https://www.w3.org/TR/xpath-functions/#flags
 467
 468        Args:
 469            name (`str`): name of group
 470            first (`bool`): return first result instead of list (default `False`)
 471            recursive (`bool`): do a recursive search of all groups/subgroups
 472            path (`list` of `str`): do group search starting from path
 473            group (`Group`): search underneath group
 474            uuid (`uuid.UUID`): group UUID
 475            regex (`bool`): whether `str` search arguments contain [XSLT style][XSLT style] regular expression
 476            flags (`str`): XPath [flags][flags]
 477
 478        The `path` list is a full path to a group (ex. `['foobar_group', 'sub_group']`).  This implies `first=True`.  All other arguments are ignored when this is given.  This is useful for handling user input.
 479
 480        The `group` argument determines what `Group` to search under, and the `recursive` boolean controls whether to search recursively.
 481
 482        The `first` (default `False`) boolean controls whether to return the first matched item, or a list of matched items.
 483
 484        - if `first=False`, the function returns a list of `Group` or `[]` if there are no matches
 485        - if `first=True`, the function returns the first `Group` match, or `None` if there are no matches
 486
 487        Returns:
 488            `list` of `Group` if `first=False`
 489            or (`Group` or `None`) if `first=True`
 490
 491        Examples:
 492        ``` python
 493        >>> kp.find_groups(name='foo', first=True)
 494        Group: "foo"
 495
 496        >>> kp.find_groups(name='foo.*', regex=True)
 497        [Group: "foo", Group "foobar"]
 498
 499        >>> kp.find_groups(path=['social'], regex=True)
 500        [Group: "social", Group: "social/foo_subgroup"]
 501
 502        >>> kp.find_groups(name='social', first=True).subgroups
 503        [Group: "social/foo_subgroup"]
 504        ```
 505        """
 506
 507        prefix = '//Group' if recursive else '/Group'
 508        res = self._find(prefix, group_xp, path=path, tree=group, **kwargs)
 509        return res
 510
 511    def add_group(self, destination_group, group_name, icon=None, notes=None):
 512        """Create a new group and all parent groups, if necessary
 513
 514        Args:
 515            destination_group (`Group`): parent group to add a new group to
 516            group_name (`str`): name of new group
 517            icon (`str`): icon name from `icons`
 518            notes (`str`): group notes
 519
 520        Returns:
 521            `Group`: newly added group
 522        """
 523        logger.debug('Creating group {}'.format(group_name))
 524
 525        if icon:
 526            group = Group(name=group_name, icon=icon, notes=notes, kp=self)
 527        else:
 528            group = Group(name=group_name, notes=notes, kp=self)
 529        destination_group.append(group)
 530
 531        return group
 532
 533    def delete_group(self, group):
 534        group.delete()
 535
 536    def move_group(self, group, destination_group):
 537        """Move a group"""
 538        destination_group.append(group)
 539
 540    def _create_or_get_recyclebin_group(self, **kwargs):
 541        existing_group = self.recyclebin_group
 542        if existing_group is not None:
 543            return existing_group
 544        kwargs.setdefault('group_name', 'Recycle Bin')
 545        group = self.add_group( self.root_group, **kwargs)
 546        elem = self._xpath('/KeePassFile/Meta/RecycleBinUUID', first=True)
 547        elem.text = base64.b64encode(group.uuid.bytes).decode('utf-8')
 548        return group
 549
 550    def trash_group(self, group):
 551        """Move a group to the RecycleBin
 552
 553        The recycle bin is created if it does not exit. ``group`` must be an empty Group.
 554
 555        Args:
 556            group (`Group`): Group to send to the RecycleBin
 557        """
 558        if not self._can_be_moved_to_recyclebin(group):
 559            raise UnableToSendToRecycleBin
 560        recyclebin_group = self._create_or_get_recyclebin_group()
 561        self.move_group( group, recyclebin_group)
 562
 563    def empty_group(self, group):
 564        """Delete all entries and subgroups of a group.
 565
 566        This does not delete the group itself
 567
 568        Args:
 569            group (`Group`): Group to empty
 570        """
 571        while len(group.subgroups):
 572            self.delete_group(group.subgroups[0])
 573        while len(group.entries):
 574            self.delete_entry(group.entries[0])
 575
 576    # ---------- Entries ----------
 577
 578
 579    from .deprecated import (
 580        find_entries_by_notes,
 581        find_entries_by_password,
 582        find_entries_by_path,
 583        find_entries_by_string,
 584        find_entries_by_title,
 585        find_entries_by_url,
 586        find_entries_by_username,
 587        find_entries_by_uuid,
 588    )
 589
 590    def find_entries(self, recursive=True, path=None, group=None, **kwargs):
 591        """Returns entries which match all provided parameters
 592        Args:
 593            path (`list` of (`str` or `None`), optional): full path to an entry
 594                (eg. `['foobar_group', 'foobar_entry']`).  This implies `first=True`.
 595                All other arguments are ignored when this is given.  This is useful for
 596                handling user input.
 597            title (`str`, optional): title of entry to find
 598            username (`str`, optional): username of entry to find
 599            password (`str`, optional): password of entry to find
 600            url (`str`, optional): url of entry to find
 601            notes (`str`, optional): notes of entry to find
 602            otp (`str`, optional): otp string of entry to find
 603            string (`dict`): custom string fields.
 604                (eg. `{'custom_field1': 'custom value', 'custom_field2': 'custom value'}`)
 605            uuid (`uuid.UUID`): entry UUID
 606            tags (`list` of `str`): entry tags
 607            autotype_enabled (`bool`, optional): autotype string is enabled
 608            autotype_sequence (`str`, optional): autotype string
 609            autotype_window (`str`, optional): autotype target window filter string
 610            group (`Group` or `None`, optional): search under this group
 611            first (`bool`, optional): return first match or `None` if no matches.
 612                Otherwise return list of `Entry` matches. (default `False`)
 613            history (`bool`): include history entries in results. (default `False`)
 614            recursive (`bool`): search recursively
 615            regex (`bool`): interpret search strings given above as
 616                [XSLT style](https://www.xml.com/pub/a/2003/06/04/tr.html) regexes
 617            flags (`str`): regex [search flags](https://www.w3.org/TR/xpath-functions/#flags)
 618
 619        Returns:
 620            `list` of `Entry` if `first=False`
 621            or (`Entry` or `None`) if `first=True`
 622
 623        Examples:
 624
 625        ``` python
 626        >>> kp.find_entries(title='gmail', first=True)
 627        Entry: "social/gmail (myusername)"
 628
 629        >>> kp.find_entries(title='foo.*', regex=True)
 630        [Entry: "foo_entry (myusername)", Entry: "foobar_entry (myusername)"]
 631
 632        >>> entry = kp.find_entries(title='foo.*', url='.*facebook.*', regex=True, first=True)
 633        >>> entry.url
 634        'facebook.com'
 635        >>> entry.title
 636        'foo_entry'
 637        >>> entry.title = 'hello'
 638
 639        >>> group = kp.find_group(name='social', first=True)
 640        >>> kp.find_entries(title='facebook', group=group, recursive=False, first=True)
 641        Entry: "social/facebook (myusername)"
 642        ```
 643        """
 644
 645        prefix = '//Entry' if recursive else '/Entry'
 646        res = self._find(prefix, entry_xp, path=path, tree=group, **kwargs)
 647
 648        return res
 649
 650
 651    def add_entry(self, destination_group, title, username,
 652                  password, url=None, notes=None, expiry_time=None,
 653                  tags=None, otp=None, icon=None, force_creation=False):
 654
 655        """Create a new entry
 656
 657        Args:
 658            destination_group (`Group`): parent group to add a new entry to
 659            title (`str`, or `None`): title of new entry
 660            username (`str` or `None`): username of new entry
 661            password (`str` or `None`): password of new entry
 662            url (`str` or `None`): URL of new entry
 663            notes (`str` or `None`): notes of new entry
 664            expiry_time (`datetime.datetime`): time of entry expiration
 665            tags (`list` of `str` or `None`): entry tags
 666            otp (`str` or `None`): OTP code of object
 667            icon (`str`, optional): icon name from `icons`
 668            force_creation (`bool`): create entry even if one with identical
 669                title exists in this group (default `False`)
 670
 671        If ``expiry_time`` is a naive datetime object
 672        (i.e. ``expiry_time.tzinfo`` is not set), the timezone is retrieved from
 673        ``dateutil.tz.gettz()``.
 674
 675
 676        Returns:
 677            `Group`: newly added group
 678        """
 679
 680        entries = self.find_entries(
 681            title=title,
 682            username=username,
 683            first=True,
 684            group=destination_group,
 685            recursive=False
 686        )
 687
 688        if entries and not force_creation:
 689            raise Exception(
 690                'An entry "{}" already exists in "{}"'.format(
 691                    title, destination_group
 692                )
 693            )
 694        else:
 695            logger.debug('Creating a new entry')
 696            entry = Entry(
 697                title=title,
 698                username=username,
 699                password=password,
 700                notes=notes,
 701                otp=otp,
 702                url=url,
 703                tags=tags,
 704                expires=True if expiry_time else False,
 705                expiry_time=expiry_time,
 706                icon=icon,
 707                kp=self
 708            )
 709            destination_group.append(entry)
 710
 711        return entry
 712
 713    def delete_entry(self, entry):
 714        """Delete entry
 715
 716        Args:
 717            entry (`Entry`): entry to delete
 718        """
 719        entry.delete()
 720
 721    def move_entry(self, entry, destination_group):
 722        """Move entry to group
 723
 724        Args:
 725            entry (`Entry`): entry to move
 726            destination_group (`Group`): group to move to
 727        """
 728        destination_group.append(entry)
 729
 730    def trash_entry(self, entry):
 731        """Move an entry to the RecycleBin
 732
 733        The recycle bin is created if it does not exit.
 734
 735        Args:
 736            entry (`Entry`): Entry to send to the RecycleBin
 737        """
 738        if not self._can_be_moved_to_recyclebin(entry):
 739            raise UnableToSendToRecycleBin
 740        recyclebin_group = self._create_or_get_recyclebin_group()
 741        self.move_entry( entry, recyclebin_group)
 742
 743    # ---------- Attachments ----------
 744
 745    def find_attachments(self, recursive=True, path=None, element=None, **kwargs):
 746        """ Find attachments in database
 747
 748        Args:
 749            id (`int` or `None`): attachment ID to match
 750            filename (`str` or `None`): filename to match
 751            element (`Entry` or `Group` or `None`): entry or group to search under
 752            recursive (`bool`): search recursively (default `True`)
 753            regex (`bool`): whether `str` search arguments contain [XSLT style][XSLT style] regular expression
 754            flags (`str`): XPath [flags][flags]
 755            history (`bool`): search under history entries. (default `False`)
 756            first (`bool`): If True, function returns first result or None.  If
 757                False, function returns list of matches or empty list.  (default
 758                `False`).
 759        """
 760
 761        prefix = '//Binary' if recursive else '/Binary'
 762        res = self._find(prefix, attachment_xp, path=path, tree=element, **kwargs)
 763
 764        return res
 765
 766    @property
 767    def attachments(self):
 768        """`list` of `Attachment`: all attachments in database"""
 769        return self.find_attachments(filename='.*', regex=True)
 770
 771    @property
 772    def binaries(self):
 773        """`list` of `bytes`: all attachment binaries in database.  The position
 774        within this list indicates the binary's ID"""
 775        if self.version >= (4, 0):
 776            # first byte is a prepended flag
 777            binaries = [a.data[1:] for a in self.payload.inner_header.binary]
 778        else:
 779            binaries = []
 780            for elem in self._xpath('/KeePassFile/Meta/Binaries/Binary'):
 781                if elem.text is not None:
 782                    if elem.get('Compressed') == 'True':
 783                        data = zlib.decompress(
 784                            base64.b64decode(elem.text),
 785                            zlib.MAX_WBITS | 32
 786                        )
 787                    else:
 788                        data = base64.b64decode(elem.text)
 789                else:
 790                    data = b''
 791                binaries.insert(int(elem.attrib['ID']), data)
 792
 793        return binaries
 794
 795    def add_binary(self, data, compressed=True, protected=True):
 796        """Add binary data to database.  Note this does not create an attachment (see `Entry.add_attachment`)
 797
 798        Args:
 799            data (`bytes`): binary data
 800            compressed (`bool`): whether binary data should be compressed.
 801                (default `True`).  Applies only to KDBX3
 802            protected (`bool`): whether protected flag should be set.  (default `True`).  Note
 803                Applies only to KDBX4
 804
 805        Returns:
 806            id (`int`): ID of binary in database
 807        """
 808        if self.version >= (4, 0):
 809            # add protected flag byte
 810            data = b'\x01' + data if protected else b'\x00' + data
 811            # add binary element to inner header
 812            c = Container(type='binary', data=data)
 813            self.payload.inner_header.binary.append(c)
 814        else:
 815            binaries = self._xpath(
 816                '/KeePassFile/Meta/Binaries',
 817                first=True
 818            )
 819            if compressed:
 820                # gzip compression
 821                compressor = zlib.compressobj(
 822                    zlib.Z_DEFAULT_COMPRESSION,
 823                    zlib.DEFLATED,
 824                    zlib.MAX_WBITS | 16
 825                )
 826                data = compressor.compress(data)
 827                data += compressor.flush()
 828            data = base64.b64encode(data).decode()
 829
 830            # set ID for Binary Element
 831            binary_id = len(self.binaries)
 832
 833            # add binary element to XML
 834            binaries.append(
 835                E.Binary(data, ID=str(binary_id), Compressed=str(compressed))
 836            )
 837
 838        # return binary id
 839        return len(self.binaries) - 1
 840
 841    def delete_binary(self, id):
 842        """Remove a binary from database and deletes attachments that reference it
 843
 844        Since attachments reference binaries by their positional index,
 845        attachments that reference binaries with ID > `id` will automatically be decremented
 846
 847        Args:
 848            id (`int`): ID of binary to remove
 849
 850        Raises:
 851            `IndexError`: raised when binary with given ID does not exist
 852        """
 853        try:
 854            if self.version >= (4, 0):
 855                # remove binary element from inner header
 856                self.payload.inner_header.binary.pop(id)
 857            else:
 858                # remove binary element from XML
 859                binaries = self._xpath('/KeePassFile/Meta/Binaries', first=True)
 860                binaries.remove(binaries.getchildren()[id])
 861        except IndexError:
 862            raise BinaryError('No such binary with id {}'.format(id))
 863
 864        # remove all entry references to this attachment
 865        for reference in self.find_attachments(id=id):
 866            reference.delete()
 867
 868        # decrement references greater than this id
 869        binaries_gt = self._xpath(
 870            '//Binary/Value[@Ref > "{}"]/..'.format(id),
 871            cast=True
 872        )
 873        for reference in binaries_gt:
 874            reference.id = reference.id - 1
 875
 876    # ---------- Misc ----------
 877
 878    def deref(self, value):
 879
 880        """Dereference [field reference][fieldref] of Entry
 881
 882        Args:
 883            ref (`str`): KeePass reference string to another field
 884
 885        Returns:
 886            `str`, `uuid.UUID` or `None` if no match found
 887
 888        [fieldref]: https://keepass.info/help/base/fieldrefs.html
 889        """
 890        if not value:
 891            return value
 892        references = set(re.findall(r'({REF:([TUPANI])@([TUPANI]):([^}]+)})', value))
 893        if not references:
 894            return value
 895        field_to_attribute = {
 896            'T': 'title',
 897            'U': 'username',
 898            'P': 'password',
 899            'A': 'url',
 900            'N': 'notes',
 901            'I': 'uuid',
 902        }
 903        for ref, wanted_field, search_in, search_value in references:
 904            wanted_field = field_to_attribute[wanted_field]
 905            search_in = field_to_attribute[search_in]
 906            if search_in == 'uuid':
 907                search_value = uuid.UUID(search_value)
 908            ref_entry = self.find_entries(first=True, **{search_in: search_value})
 909            if ref_entry is None:
 910                return None
 911            value = value.replace(ref, getattr(ref_entry, wanted_field))
 912        return self.deref(value)
 913
 914
 915    # ---------- Credential Changing and Expiry ----------
 916
 917    @property
 918    def password(self):
 919        """`str`: Get or set database password"""
 920        return self._password
 921
 922    @password.setter
 923    def password(self, password):
 924        self._password = password
 925        self.credchange_date = datetime.now(timezone.utc)
 926
 927    @property
 928    def keyfile(self):
 929        """`str` or `pathlib.Path` or `None`: get or set database keyfile"""
 930        return self._keyfile
 931
 932    @keyfile.setter
 933    def keyfile(self, keyfile):
 934        self._keyfile = keyfile
 935        self.credchange_date = datetime.now(timezone.utc)
 936
 937    @property
 938    def credchange_required_days(self):
 939        """`int`: Days until password update should be required"""
 940        e = self._xpath('/KeePassFile/Meta/MasterKeyChangeForce', first=True)
 941        if e is not None:
 942            return int(e.text)
 943
 944    @property
 945    def credchange_recommended_days(self):
 946        """`int`: Days until password update should be recommended"""
 947        e = self._xpath('/KeePassFile/Meta/MasterKeyChangeRec', first=True)
 948        if e is not None:
 949            return int(e.text)
 950
 951    @credchange_required_days.setter
 952    def credchange_required_days(self, days):
 953        path = '/KeePassFile/Meta/MasterKeyChangeForce'
 954        item = self._xpath(path, first=True)
 955        item.text = str(days)
 956
 957    @credchange_recommended_days.setter
 958    def credchange_recommended_days(self, days):
 959        path = '/KeePassFile/Meta/MasterKeyChangeRec'
 960        item = self._xpath(path, first=True)
 961        item.text = str(days)
 962
 963    @property
 964    def credchange_date(self):
 965        """`datetime.datetime`: get or set UTC time of last credential change"""
 966        e = self._xpath('/KeePassFile/Meta/MasterKeyChanged', first=True)
 967        if e is not None:
 968            return self._decode_time(e.text)
 969
 970    @credchange_date.setter
 971    def credchange_date(self, date):
 972        mk_time = self._xpath('/KeePassFile/Meta/MasterKeyChanged', first=True)
 973        mk_time.text = self._encode_time(date)
 974
 975    @property
 976    def credchange_required(self):
 977        """`bool`: Check if credential change is required"""
 978        change_date = self.credchange_date
 979        if change_date is None or self.credchange_required_days == -1:
 980            return False
 981        now_date = datetime.now(timezone.utc)
 982        return (now_date - change_date).days > self.credchange_required_days
 983
 984    @property
 985    def credchange_recommended(self):
 986        """`bool`: Check if credential change is recommended"""
 987        change_date = self.credchange_date
 988        if change_date is None or self.credchange_recommended_days == -1:
 989            return False
 990        now_date = datetime.now(timezone.utc)
 991        return (now_date - change_date).days > self.credchange_recommended_days
 992
 993    # ---------- Datetime Functions ----------
 994
 995    def _encode_time(self, value):
 996        """`bytes` or `str`: Convert datetime to base64 or plaintext string"""
 997
 998        if self.version >= (4, 0):
 999            diff_seconds = int(
1000                (
1001                    value -
1002                    datetime(
1003                        year=1,
1004                        month=1,
1005                        day=1,
1006                        tzinfo=timezone.utc
1007                    )
1008                ).total_seconds()
1009            )
1010            return base64.b64encode(
1011                struct.pack('<Q', diff_seconds)
1012            ).decode('utf-8')
1013        else:
1014            return value.isoformat()
1015
1016    def _decode_time(self, text):
1017        """`datetime.datetime`: Convert base64 time or plaintext time to datetime"""
1018
1019        if self.version >= (4, 0):
1020            # decode KDBX4 date from b64 format
1021            try:
1022                return (
1023                    datetime(year=1, month=1, day=1, tzinfo=timezone.utc) +
1024                    timedelta(
1025                        seconds=struct.unpack('<Q', base64.b64decode(text))[0]
1026                    )
1027                )
1028            except BinasciiError:
1029                return datetime.fromisoformat(text.replace('Z','+00:00')).replace(tzinfo=timezone.utc)
1030        else:
1031            return datetime.fromisoformat(text.replace('Z','+00:00')).replace(tzinfo=timezone.utc)

Open a KeePass database

Arguments:
  • filename (str, optional): path to database or stream object. If None, the path given when the database was opened is used.
  • password (str, optional): database password. If None, database is assumed to have no password
  • keyfile (str, optional): path to keyfile. If None, database is assumed to have no keyfile
  • transformed_key (bytes, optional): precomputed transformed key.
  • decrypt (bool, optional): whether to decrypt XML payload. Set False to access outer header information without decrypting database.
Raises:
  • CredentialsError: raised when password/keyfile or transformed key are wrong
  • HeaderChecksumError: raised when checksum in database header is is wrong. e.g. database tampering or file corruption
  • PayloadChecksumError: raised when payload blocks checksum is wrong, e.g. corruption during database saving
PyKeePass( filename, password=None, keyfile=None, transformed_key=None, decrypt=True)
66    def __init__(self, filename, password=None, keyfile=None,
67                 transformed_key=None, decrypt=True):
68
69        self.read(
70            filename=filename,
71            password=password,
72            keyfile=keyfile,
73            transformed_key=transformed_key,
74            decrypt=decrypt
75        )
def read( self, filename=None, password=None, keyfile=None, transformed_key=None, decrypt=True):
 84    def read(self, filename=None, password=None, keyfile=None,
 85             transformed_key=None, decrypt=True):
 86        """
 87        See class docstring.
 88        """
 89
 90        # TODO: - raise, no filename provided, database not open
 91        self._password = password
 92        self._keyfile = keyfile
 93        if filename:
 94            self.filename = filename
 95        else:
 96            filename = self.filename
 97
 98        try:
 99            if hasattr(filename, "read"):
100                self.kdbx = KDBX.parse_stream(
101                    filename,
102                    password=password,
103                    keyfile=keyfile,
104                    transformed_key=transformed_key,
105                    decrypt=decrypt
106                )
107            else:
108                self.kdbx = KDBX.parse_file(
109                    filename,
110                    password=password,
111                    keyfile=keyfile,
112                    transformed_key=transformed_key,
113                    decrypt=decrypt
114                )
115
116        except CheckError as e:
117            if e.path == '(parsing) -> header -> sig_check':
118                raise HeaderChecksumError("Not a KeePass database")
119            else:
120                raise
121
122        # body integrity/verification
123        except ChecksumError as e:
124            if e.path in (
125                    '(parsing) -> body -> cred_check', # KDBX4
126                    '(parsing) -> cred_check' # KDBX3
127                    ):
128                raise CredentialsError("Invalid credentials")
129            elif e.path == '(parsing) -> body -> sha256':
130                raise HeaderChecksumError("Corrupted database")
131            elif e.path in (
132                    '(parsing) -> body -> payload -> hmac_hash', # KDBX4
133                    '(parsing) -> xml -> block_hash' # KDBX3
134                    ):
135                raise PayloadChecksumError("Error reading database contents")
136            else:
137                raise

See class docstring.

def reload(self):
139    def reload(self):
140        """Reload current database using previously given credentials """
141
142        self.read(self.filename, self.password, self.keyfile)

Reload current database using previously given credentials

def save(self, filename=None, transformed_key=None):
144    def save(self, filename=None, transformed_key=None):
145        """Save current database object to disk.
146
147        Args:
148            filename (`str`, optional): path to database or stream object.
149                If None, the path given when the database was opened is used.
150                PyKeePass.filename is unchanged.
151            transformed_key (`bytes`, optional): precomputed transformed
152                key.
153        """
154
155        if not filename:
156            filename = self.filename
157
158        if hasattr(filename, "write"):
159            KDBX.build_stream(
160                self.kdbx,
161                filename,
162                password=self.password,
163                keyfile=self.keyfile,
164                transformed_key=transformed_key,
165                decrypt=True
166            )
167        else:
168            # save to temporary file to prevent database clobbering
169            # see issues 223, 101
170            filename_tmp = Path(filename).with_suffix('.tmp')
171            try:
172                KDBX.build_file(
173                    self.kdbx,
174                    filename_tmp,
175                    password=self.password,
176                    keyfile=self.keyfile,
177                    transformed_key=transformed_key,
178                    decrypt=True
179                )
180            except Exception as e:
181                os.remove(filename_tmp)
182                raise e
183            shutil.move(filename_tmp, filename)

Save current database object to disk.

Arguments:
  • filename (str, optional): path to database or stream object. If None, the path given when the database was opened is used. PyKeePass.filename is unchanged.
  • transformed_key (bytes, optional): precomputed transformed key.
version
185    @property
186    def version(self):
187        """`tuple` of `int`: Length 2 tuple of ints containing major and minor versions.
188        Generally (3, 1) or (4, 0)."""
189        return (
190            self.kdbx.header.value.major_version,
191            self.kdbx.header.value.minor_version
192        )

tuple of int: Length 2 tuple of ints containing major and minor versions. Generally (3, 1) or (4, 0).

encryption_algorithm
194    @property
195    def encryption_algorithm(self):
196        """`str`: encryption algorithm used by database during decryption.
197        Can be one of 'aes256', 'chacha20', or 'twofish'."""
198        return self.kdbx.header.value.dynamic_header.cipher_id.data

str: encryption algorithm used by database during decryption. Can be one of 'aes256', 'chacha20', or 'twofish'.

kdf_algorithm
200    @property
201    def kdf_algorithm(self):
202        """`str`: key derivation algorithm used by database during decryption.
203        Can be one of 'aeskdf', 'argon2', or 'aeskdf'"""
204        if self.version == (3, 1):
205            return 'aeskdf'
206        elif self.version == (4, 0):
207            kdf_parameters = self.kdbx.header.value.dynamic_header.kdf_parameters.data.dict
208            if kdf_parameters['$UUID'].value == kdf_uuids['argon2']:
209                return 'argon2'
210            elif kdf_parameters['$UUID'].value == kdf_uuids['argon2id']:
211                return 'argon2id'
212            elif kdf_parameters['$UUID'].value == kdf_uuids['aeskdf']:
213                return 'aeskdf'

str: key derivation algorithm used by database during decryption. Can be one of 'aeskdf', 'argon2', or 'aeskdf'

transformed_key
215    @property
216    def transformed_key(self):
217        """`bytes`: transformed key used in database decryption.  May be cached
218        and passed to `open` for faster database opening"""
219        return self.kdbx.body.transformed_key

bytes: transformed key used in database decryption. May be cached and passed to open for faster database opening

database_salt
221    @property
222    def database_salt(self):
223       """`bytes`: salt of database kdf. This can be used for adding additional
224       credentials which are used in extension to current keyfile."""
225
226       if self.version == (3, 1):
227            return self.kdbx.header.value.dynamic_header.transform_seed.data
228
229       kdf_parameters = self.kdbx.header.value.dynamic_header.kdf_parameters.data.dict
230       return kdf_parameters['S'].value

bytes: salt of database kdf. This can be used for adding additional credentials which are used in extension to current keyfile.

payload
232    @property
233    def payload(self):
234        """`construct.Container`: Encrypted payload of keepass database"""
235
236        # check if payload is decrypted
237        if self.kdbx.body.payload is None:
238            raise ValueError("Database is not decrypted")
239        else:
240            return self.kdbx.body.payload

construct.Container: Encrypted payload of keepass database

tree
242    @property
243    def tree(self):
244        """`lxml.etree._ElementTree`: database XML payload"""
245        return self.payload.xml

lxml.etree._ElementTree: database XML payload

root_group
247    @property
248    def root_group(self):
249        """`Group`: root Group of database"""
250        return self.find_groups(path='', first=True)

Group: root Group of database

recyclebin_group
252    @property
253    def recyclebin_group(self):
254        """`Group`: RecycleBin Group of database"""
255        elem = self._xpath('/KeePassFile/Meta/RecycleBinUUID', first=True)
256        recyclebin_uuid = uuid.UUID( bytes = base64.b64decode(elem.text) )
257        return self.find_groups(uuid=recyclebin_uuid, first=True)

Group: RecycleBin Group of database

groups
259    @property
260    def groups(self):
261        """`list` of `Group`: all groups in database
262        """
263        return self.find_groups()

list of Group: all groups in database

entries
265    @property
266    def entries(self):
267        """`list` of `Entry`: all entries in database,
268        excluding history"""
269        return self.find_entries()

list of Entry: all entries in database, excluding history

database_name
271    @property
272    def database_name(self):
273        """`str`: Name of database"""
274        elem = self._xpath('/KeePassFile/Meta/DatabaseName', first=True)
275        return elem.text

str: Name of database

database_description
282    @property
283    def database_description(self):
284        """`str`: Description of the database"""
285        elem = self._xpath('/KeePassFile/Meta/DatabaseDescription', first=True)
286        return elem.text

str: Description of the database

default_username
293    @property
294    def default_username(self):
295        """`str` or `None`: default user.  `None` if not set"""
296        elem = self._xpath('/KeePassFile/Meta/DefaultUserName', first=True)
297        return elem.text

str or None: default user. None if not set

def xml(self):
304    def xml(self):
305        """Get XML part of database as string
306
307        Returns:
308            `str`: XML content of database
309        """
310        return etree.tostring(
311            self.tree,
312            pretty_print=True,
313            standalone=True,
314            encoding='utf-8'
315        )

Get XML part of database as string

Returns:

str: XML content of database

def dump_xml(self, filename):
317    def dump_xml(self, filename):
318        """ Dump the contents of the database to file as XML
319
320        Args:
321            filename (`str`): path to output file
322        """
323        with open(filename, 'wb') as f:
324            f.write(self.xml())

Dump the contents of the database to file as XML

Arguments:
  • filename (str): path to output file
def xpath(self, xpath_str, tree=None, first=False, cast=False, **kwargs):
326    def xpath(self, xpath_str, tree=None, first=False, cast=False, **kwargs):
327        """Look up elements in the XML payload and return corresponding object.
328
329        Internal function which searches the payload lxml ElementTree for
330        elements via XPath.  Matched entry, group, and attachment elements are
331        automatically cast to their corresponding objects, otherwise an error
332        is raised.
333
334        Args:
335            xpath_str (`str`): XPath query for finding element(s)
336            tree (`_ElementTree`, `Element`, optional): use this
337                element as root node when searching
338            first (`bool`): If True, function returns first result or None.  If
339                False, function returns list of matches or empty list.
340                    (default `False`).
341            cast (`bool`): If True, matches are instead instantiated as
342                pykeepass Group, Entry, or Attachment objects.  An exception
343                is raised if a match cannot be cast.  (default `False`)
344
345        Returns:
346            `list` of `Group`, `Entry`, `Attachment`, or `lxml.etree.Element`
347        """
348
349        if tree is None:
350            tree = self.tree
351        logger.debug('xpath query: ' + xpath_str)
352        elements = tree.xpath(
353            xpath_str, namespaces={'re': 'http://exslt.org/regular-expressions'}
354        )
355
356        res = []
357        for e in elements:
358            if cast:
359                if e.tag == 'Entry':
360                    res.append(Entry(element=e, kp=self))
361                elif e.tag == 'Group':
362                    res.append(Group(element=e, kp=self))
363                elif e.tag == 'Binary' and e.getparent().tag == 'Entry':
364                    res.append(Attachment(element=e, kp=self))
365                else:
366                    raise Exception('Could not cast element {}'.format(e))
367            else:
368                res.append(e)
369
370        # return first object in list or None
371        if first:
372            res = res[0] if res else None
373
374        return res

Look up elements in the XML payload and return corresponding object.

Internal function which searches the payload lxml ElementTree for elements via XPath. Matched entry, group, and attachment elements are automatically cast to their corresponding objects, otherwise an error is raised.

Arguments:
  • xpath_str (str): XPath query for finding element(s)
  • tree (_ElementTree, Element, optional): use this element as root node when searching
  • first (bool): If True, function returns first result or None. If False, function returns list of matches or empty list. (default False).
  • cast (bool): If True, matches are instead instantiated as pykeepass Group, Entry, or Attachment objects. An exception is raised if a match cannot be cast. (default False)
Returns:

list of Group, Entry, Attachment, or lxml.etree.Element

def find_groups(self, recursive=True, path=None, group=None, **kwargs):
462    def find_groups(self, recursive=True, path=None, group=None, **kwargs):
463        """ Find groups in a database
464
465        [XSLT style]: https://www.xml.com/pub/a/2003/06/04/tr.html
466        [flags]: https://www.w3.org/TR/xpath-functions/#flags
467
468        Args:
469            name (`str`): name of group
470            first (`bool`): return first result instead of list (default `False`)
471            recursive (`bool`): do a recursive search of all groups/subgroups
472            path (`list` of `str`): do group search starting from path
473            group (`Group`): search underneath group
474            uuid (`uuid.UUID`): group UUID
475            regex (`bool`): whether `str` search arguments contain [XSLT style][XSLT style] regular expression
476            flags (`str`): XPath [flags][flags]
477
478        The `path` list is a full path to a group (ex. `['foobar_group', 'sub_group']`).  This implies `first=True`.  All other arguments are ignored when this is given.  This is useful for handling user input.
479
480        The `group` argument determines what `Group` to search under, and the `recursive` boolean controls whether to search recursively.
481
482        The `first` (default `False`) boolean controls whether to return the first matched item, or a list of matched items.
483
484        - if `first=False`, the function returns a list of `Group` or `[]` if there are no matches
485        - if `first=True`, the function returns the first `Group` match, or `None` if there are no matches
486
487        Returns:
488            `list` of `Group` if `first=False`
489            or (`Group` or `None`) if `first=True`
490
491        Examples:
492        ``` python
493        >>> kp.find_groups(name='foo', first=True)
494        Group: "foo"
495
496        >>> kp.find_groups(name='foo.*', regex=True)
497        [Group: "foo", Group "foobar"]
498
499        >>> kp.find_groups(path=['social'], regex=True)
500        [Group: "social", Group: "social/foo_subgroup"]
501
502        >>> kp.find_groups(name='social', first=True).subgroups
503        [Group: "social/foo_subgroup"]
504        ```
505        """
506
507        prefix = '//Group' if recursive else '/Group'
508        res = self._find(prefix, group_xp, path=path, tree=group, **kwargs)
509        return res

Find groups in a database

Arguments:
  • name (str): name of group
  • first (bool): return first result instead of list (default False)
  • recursive (bool): do a recursive search of all groups/subgroups
  • path (list of str): do group search starting from path
  • group (Group): search underneath group
  • uuid (uuid.UUID): group UUID
  • regex (bool): whether str search arguments contain XSLT style regular expression
  • flags (str): XPath flags

The path list is a full path to a group (ex. ['foobar_group', 'sub_group']). This implies first=True. All other arguments are ignored when this is given. This is useful for handling user input.

The group argument determines what Group to search under, and the recursive boolean controls whether to search recursively.

The first (default False) boolean controls whether to return the first matched item, or a list of matched items.

  • if first=False, the function returns a list of Group or [] if there are no matches
  • if first=True, the function returns the first Group match, or None if there are no matches
Returns:

list of Group if first=False or (Group or None) if first=True

Examples:

>>> kp.find_groups(name='foo', first=True)
Group: "foo"

>>> kp.find_groups(name='foo.*', regex=True)
[Group: "foo", Group "foobar"]

>>> kp.find_groups(path=['social'], regex=True)
[Group: "social", Group: "social/foo_subgroup"]

>>> kp.find_groups(name='social', first=True).subgroups
[Group: "social/foo_subgroup"]
def add_group(self, destination_group, group_name, icon=None, notes=None):
511    def add_group(self, destination_group, group_name, icon=None, notes=None):
512        """Create a new group and all parent groups, if necessary
513
514        Args:
515            destination_group (`Group`): parent group to add a new group to
516            group_name (`str`): name of new group
517            icon (`str`): icon name from `icons`
518            notes (`str`): group notes
519
520        Returns:
521            `Group`: newly added group
522        """
523        logger.debug('Creating group {}'.format(group_name))
524
525        if icon:
526            group = Group(name=group_name, icon=icon, notes=notes, kp=self)
527        else:
528            group = Group(name=group_name, notes=notes, kp=self)
529        destination_group.append(group)
530
531        return group

Create a new group and all parent groups, if necessary

Arguments:
  • destination_group (Group): parent group to add a new group to
  • group_name (str): name of new group
  • icon (str): icon name from icons
  • notes (str): group notes
Returns:

Group: newly added group

def delete_group(self, group):
533    def delete_group(self, group):
534        group.delete()
def move_group(self, group, destination_group):
536    def move_group(self, group, destination_group):
537        """Move a group"""
538        destination_group.append(group)

Move a group

def trash_group(self, group):
550    def trash_group(self, group):
551        """Move a group to the RecycleBin
552
553        The recycle bin is created if it does not exit. ``group`` must be an empty Group.
554
555        Args:
556            group (`Group`): Group to send to the RecycleBin
557        """
558        if not self._can_be_moved_to_recyclebin(group):
559            raise UnableToSendToRecycleBin
560        recyclebin_group = self._create_or_get_recyclebin_group()
561        self.move_group( group, recyclebin_group)

Move a group to the RecycleBin

The recycle bin is created if it does not exit. group must be an empty Group.

Arguments:
  • group (Group): Group to send to the RecycleBin
def empty_group(self, group):
563    def empty_group(self, group):
564        """Delete all entries and subgroups of a group.
565
566        This does not delete the group itself
567
568        Args:
569            group (`Group`): Group to empty
570        """
571        while len(group.subgroups):
572            self.delete_group(group.subgroups[0])
573        while len(group.entries):
574            self.delete_entry(group.entries[0])

Delete all entries and subgroups of a group.

This does not delete the group itself

Arguments:
  • group (Group): Group to empty
def find_entries(self, recursive=True, path=None, group=None, **kwargs):
590    def find_entries(self, recursive=True, path=None, group=None, **kwargs):
591        """Returns entries which match all provided parameters
592        Args:
593            path (`list` of (`str` or `None`), optional): full path to an entry
594                (eg. `['foobar_group', 'foobar_entry']`).  This implies `first=True`.
595                All other arguments are ignored when this is given.  This is useful for
596                handling user input.
597            title (`str`, optional): title of entry to find
598            username (`str`, optional): username of entry to find
599            password (`str`, optional): password of entry to find
600            url (`str`, optional): url of entry to find
601            notes (`str`, optional): notes of entry to find
602            otp (`str`, optional): otp string of entry to find
603            string (`dict`): custom string fields.
604                (eg. `{'custom_field1': 'custom value', 'custom_field2': 'custom value'}`)
605            uuid (`uuid.UUID`): entry UUID
606            tags (`list` of `str`): entry tags
607            autotype_enabled (`bool`, optional): autotype string is enabled
608            autotype_sequence (`str`, optional): autotype string
609            autotype_window (`str`, optional): autotype target window filter string
610            group (`Group` or `None`, optional): search under this group
611            first (`bool`, optional): return first match or `None` if no matches.
612                Otherwise return list of `Entry` matches. (default `False`)
613            history (`bool`): include history entries in results. (default `False`)
614            recursive (`bool`): search recursively
615            regex (`bool`): interpret search strings given above as
616                [XSLT style](https://www.xml.com/pub/a/2003/06/04/tr.html) regexes
617            flags (`str`): regex [search flags](https://www.w3.org/TR/xpath-functions/#flags)
618
619        Returns:
620            `list` of `Entry` if `first=False`
621            or (`Entry` or `None`) if `first=True`
622
623        Examples:
624
625        ``` python
626        >>> kp.find_entries(title='gmail', first=True)
627        Entry: "social/gmail (myusername)"
628
629        >>> kp.find_entries(title='foo.*', regex=True)
630        [Entry: "foo_entry (myusername)", Entry: "foobar_entry (myusername)"]
631
632        >>> entry = kp.find_entries(title='foo.*', url='.*facebook.*', regex=True, first=True)
633        >>> entry.url
634        'facebook.com'
635        >>> entry.title
636        'foo_entry'
637        >>> entry.title = 'hello'
638
639        >>> group = kp.find_group(name='social', first=True)
640        >>> kp.find_entries(title='facebook', group=group, recursive=False, first=True)
641        Entry: "social/facebook (myusername)"
642        ```
643        """
644
645        prefix = '//Entry' if recursive else '/Entry'
646        res = self._find(prefix, entry_xp, path=path, tree=group, **kwargs)
647
648        return res

Returns entries which match all provided parameters

Arguments:
  • path (list of (str or None), optional): full path to an entry (eg. ['foobar_group', 'foobar_entry']). This implies first=True. All other arguments are ignored when this is given. This is useful for handling user input.
  • title (str, optional): title of entry to find
  • username (str, optional): username of entry to find
  • password (str, optional): password of entry to find
  • url (str, optional): url of entry to find
  • notes (str, optional): notes of entry to find
  • otp (str, optional): otp string of entry to find
  • string (dict): custom string fields. (eg. {'custom_field1': 'custom value', 'custom_field2': 'custom value'})
  • uuid (uuid.UUID): entry UUID
  • tags (list of str): entry tags
  • autotype_enabled (bool, optional): autotype string is enabled
  • autotype_sequence (str, optional): autotype string
  • autotype_window (str, optional): autotype target window filter string
  • group (Group or None, optional): search under this group
  • first (bool, optional): return first match or None if no matches. Otherwise return list of Entry matches. (default False)
  • history (bool): include history entries in results. (default False)
  • recursive (bool): search recursively
  • regex (bool): interpret search strings given above as XSLT style regexes
  • flags (str): regex search flags
Returns:

list of Entry if first=False or (Entry or None) if first=True

Examples:

>>> kp.find_entries(title='gmail', first=True)
Entry: "social/gmail (myusername)"

>>> kp.find_entries(title='foo.*', regex=True)
[Entry: "foo_entry (myusername)", Entry: "foobar_entry (myusername)"]

>>> entry = kp.find_entries(title='foo.*', url='.*facebook.*', regex=True, first=True)
>>> entry.url
'facebook.com'
>>> entry.title
'foo_entry'
>>> entry.title = 'hello'

>>> group = kp.find_group(name='social', first=True)
>>> kp.find_entries(title='facebook', group=group, recursive=False, first=True)
Entry: "social/facebook (myusername)"
def add_entry( self, destination_group, title, username, password, url=None, notes=None, expiry_time=None, tags=None, otp=None, icon=None, force_creation=False):
651    def add_entry(self, destination_group, title, username,
652                  password, url=None, notes=None, expiry_time=None,
653                  tags=None, otp=None, icon=None, force_creation=False):
654
655        """Create a new entry
656
657        Args:
658            destination_group (`Group`): parent group to add a new entry to
659            title (`str`, or `None`): title of new entry
660            username (`str` or `None`): username of new entry
661            password (`str` or `None`): password of new entry
662            url (`str` or `None`): URL of new entry
663            notes (`str` or `None`): notes of new entry
664            expiry_time (`datetime.datetime`): time of entry expiration
665            tags (`list` of `str` or `None`): entry tags
666            otp (`str` or `None`): OTP code of object
667            icon (`str`, optional): icon name from `icons`
668            force_creation (`bool`): create entry even if one with identical
669                title exists in this group (default `False`)
670
671        If ``expiry_time`` is a naive datetime object
672        (i.e. ``expiry_time.tzinfo`` is not set), the timezone is retrieved from
673        ``dateutil.tz.gettz()``.
674
675
676        Returns:
677            `Group`: newly added group
678        """
679
680        entries = self.find_entries(
681            title=title,
682            username=username,
683            first=True,
684            group=destination_group,
685            recursive=False
686        )
687
688        if entries and not force_creation:
689            raise Exception(
690                'An entry "{}" already exists in "{}"'.format(
691                    title, destination_group
692                )
693            )
694        else:
695            logger.debug('Creating a new entry')
696            entry = Entry(
697                title=title,
698                username=username,
699                password=password,
700                notes=notes,
701                otp=otp,
702                url=url,
703                tags=tags,
704                expires=True if expiry_time else False,
705                expiry_time=expiry_time,
706                icon=icon,
707                kp=self
708            )
709            destination_group.append(entry)
710
711        return entry

Create a new entry

Arguments:
  • destination_group (Group): parent group to add a new entry to
  • title (str, or None): title of new entry
  • username (str or None): username of new entry
  • password (str or None): password of new entry
  • url (str or None): URL of new entry
  • notes (str or None): notes of new entry
  • expiry_time (datetime.datetime): time of entry expiration
  • tags (list of str or None): entry tags
  • otp (str or None): OTP code of object
  • icon (str, optional): icon name from icons
  • force_creation (bool): create entry even if one with identical title exists in this group (default False)

If expiry_time is a naive datetime object (i.e. expiry_time.tzinfo is not set), the timezone is retrieved from dateutil.tz.gettz().

Returns:

Group: newly added group

def delete_entry(self, entry):
713    def delete_entry(self, entry):
714        """Delete entry
715
716        Args:
717            entry (`Entry`): entry to delete
718        """
719        entry.delete()

Delete entry

Arguments:
  • entry (Entry): entry to delete
def move_entry(self, entry, destination_group):
721    def move_entry(self, entry, destination_group):
722        """Move entry to group
723
724        Args:
725            entry (`Entry`): entry to move
726            destination_group (`Group`): group to move to
727        """
728        destination_group.append(entry)

Move entry to group

Arguments:
  • entry (Entry): entry to move
  • destination_group (Group): group to move to
def trash_entry(self, entry):
730    def trash_entry(self, entry):
731        """Move an entry to the RecycleBin
732
733        The recycle bin is created if it does not exit.
734
735        Args:
736            entry (`Entry`): Entry to send to the RecycleBin
737        """
738        if not self._can_be_moved_to_recyclebin(entry):
739            raise UnableToSendToRecycleBin
740        recyclebin_group = self._create_or_get_recyclebin_group()
741        self.move_entry( entry, recyclebin_group)

Move an entry to the RecycleBin

The recycle bin is created if it does not exit.

Arguments:
  • entry (Entry): Entry to send to the RecycleBin
def find_attachments(self, recursive=True, path=None, element=None, **kwargs):
745    def find_attachments(self, recursive=True, path=None, element=None, **kwargs):
746        """ Find attachments in database
747
748        Args:
749            id (`int` or `None`): attachment ID to match
750            filename (`str` or `None`): filename to match
751            element (`Entry` or `Group` or `None`): entry or group to search under
752            recursive (`bool`): search recursively (default `True`)
753            regex (`bool`): whether `str` search arguments contain [XSLT style][XSLT style] regular expression
754            flags (`str`): XPath [flags][flags]
755            history (`bool`): search under history entries. (default `False`)
756            first (`bool`): If True, function returns first result or None.  If
757                False, function returns list of matches or empty list.  (default
758                `False`).
759        """
760
761        prefix = '//Binary' if recursive else '/Binary'
762        res = self._find(prefix, attachment_xp, path=path, tree=element, **kwargs)
763
764        return res

Find attachments in database

Arguments:
  • id (int or None): attachment ID to match
  • filename (str or None): filename to match
  • element (Entry or Group or None): entry or group to search under
  • recursive (bool): search recursively (default True)
  • regex (bool): whether str search arguments contain [XSLT style][XSLT style] regular expression
  • flags (str): XPath [flags][flags]
  • history (bool): search under history entries. (default False)
  • first (bool): If True, function returns first result or None. If False, function returns list of matches or empty list. (default False).
attachments
766    @property
767    def attachments(self):
768        """`list` of `Attachment`: all attachments in database"""
769        return self.find_attachments(filename='.*', regex=True)

list of Attachment: all attachments in database

binaries
771    @property
772    def binaries(self):
773        """`list` of `bytes`: all attachment binaries in database.  The position
774        within this list indicates the binary's ID"""
775        if self.version >= (4, 0):
776            # first byte is a prepended flag
777            binaries = [a.data[1:] for a in self.payload.inner_header.binary]
778        else:
779            binaries = []
780            for elem in self._xpath('/KeePassFile/Meta/Binaries/Binary'):
781                if elem.text is not None:
782                    if elem.get('Compressed') == 'True':
783                        data = zlib.decompress(
784                            base64.b64decode(elem.text),
785                            zlib.MAX_WBITS | 32
786                        )
787                    else:
788                        data = base64.b64decode(elem.text)
789                else:
790                    data = b''
791                binaries.insert(int(elem.attrib['ID']), data)
792
793        return binaries

list of bytes: all attachment binaries in database. The position within this list indicates the binary's ID

def add_binary(self, data, compressed=True, protected=True):
795    def add_binary(self, data, compressed=True, protected=True):
796        """Add binary data to database.  Note this does not create an attachment (see `Entry.add_attachment`)
797
798        Args:
799            data (`bytes`): binary data
800            compressed (`bool`): whether binary data should be compressed.
801                (default `True`).  Applies only to KDBX3
802            protected (`bool`): whether protected flag should be set.  (default `True`).  Note
803                Applies only to KDBX4
804
805        Returns:
806            id (`int`): ID of binary in database
807        """
808        if self.version >= (4, 0):
809            # add protected flag byte
810            data = b'\x01' + data if protected else b'\x00' + data
811            # add binary element to inner header
812            c = Container(type='binary', data=data)
813            self.payload.inner_header.binary.append(c)
814        else:
815            binaries = self._xpath(
816                '/KeePassFile/Meta/Binaries',
817                first=True
818            )
819            if compressed:
820                # gzip compression
821                compressor = zlib.compressobj(
822                    zlib.Z_DEFAULT_COMPRESSION,
823                    zlib.DEFLATED,
824                    zlib.MAX_WBITS | 16
825                )
826                data = compressor.compress(data)
827                data += compressor.flush()
828            data = base64.b64encode(data).decode()
829
830            # set ID for Binary Element
831            binary_id = len(self.binaries)
832
833            # add binary element to XML
834            binaries.append(
835                E.Binary(data, ID=str(binary_id), Compressed=str(compressed))
836            )
837
838        # return binary id
839        return len(self.binaries) - 1

Add binary data to database. Note this does not create an attachment (see Entry.add_attachment)

Arguments:
  • data (bytes): binary data
  • compressed (bool): whether binary data should be compressed. (default True). Applies only to KDBX3
  • protected (bool): whether protected flag should be set. (default True). Note Applies only to KDBX4
Returns:

id (int): ID of binary in database

def delete_binary(self, id):
841    def delete_binary(self, id):
842        """Remove a binary from database and deletes attachments that reference it
843
844        Since attachments reference binaries by their positional index,
845        attachments that reference binaries with ID > `id` will automatically be decremented
846
847        Args:
848            id (`int`): ID of binary to remove
849
850        Raises:
851            `IndexError`: raised when binary with given ID does not exist
852        """
853        try:
854            if self.version >= (4, 0):
855                # remove binary element from inner header
856                self.payload.inner_header.binary.pop(id)
857            else:
858                # remove binary element from XML
859                binaries = self._xpath('/KeePassFile/Meta/Binaries', first=True)
860                binaries.remove(binaries.getchildren()[id])
861        except IndexError:
862            raise BinaryError('No such binary with id {}'.format(id))
863
864        # remove all entry references to this attachment
865        for reference in self.find_attachments(id=id):
866            reference.delete()
867
868        # decrement references greater than this id
869        binaries_gt = self._xpath(
870            '//Binary/Value[@Ref > "{}"]/..'.format(id),
871            cast=True
872        )
873        for reference in binaries_gt:
874            reference.id = reference.id - 1

Remove a binary from database and deletes attachments that reference it

Since attachments reference binaries by their positional index, attachments that reference binaries with ID > id will automatically be decremented

Arguments:
  • id (int): ID of binary to remove
Raises:
  • IndexError: raised when binary with given ID does not exist
def deref(self, value):
878    def deref(self, value):
879
880        """Dereference [field reference][fieldref] of Entry
881
882        Args:
883            ref (`str`): KeePass reference string to another field
884
885        Returns:
886            `str`, `uuid.UUID` or `None` if no match found
887
888        [fieldref]: https://keepass.info/help/base/fieldrefs.html
889        """
890        if not value:
891            return value
892        references = set(re.findall(r'({REF:([TUPANI])@([TUPANI]):([^}]+)})', value))
893        if not references:
894            return value
895        field_to_attribute = {
896            'T': 'title',
897            'U': 'username',
898            'P': 'password',
899            'A': 'url',
900            'N': 'notes',
901            'I': 'uuid',
902        }
903        for ref, wanted_field, search_in, search_value in references:
904            wanted_field = field_to_attribute[wanted_field]
905            search_in = field_to_attribute[search_in]
906            if search_in == 'uuid':
907                search_value = uuid.UUID(search_value)
908            ref_entry = self.find_entries(first=True, **{search_in: search_value})
909            if ref_entry is None:
910                return None
911            value = value.replace(ref, getattr(ref_entry, wanted_field))
912        return self.deref(value)

Dereference field reference of Entry

Arguments:
  • ref (str): KeePass reference string to another field
Returns:

str, uuid.UUID or None if no match found

password
917    @property
918    def password(self):
919        """`str`: Get or set database password"""
920        return self._password

str: Get or set database password

keyfile
927    @property
928    def keyfile(self):
929        """`str` or `pathlib.Path` or `None`: get or set database keyfile"""
930        return self._keyfile

str or pathlib.Path or None: get or set database keyfile

credchange_required_days
937    @property
938    def credchange_required_days(self):
939        """`int`: Days until password update should be required"""
940        e = self._xpath('/KeePassFile/Meta/MasterKeyChangeForce', first=True)
941        if e is not None:
942            return int(e.text)

int: Days until password update should be required

credchange_date
963    @property
964    def credchange_date(self):
965        """`datetime.datetime`: get or set UTC time of last credential change"""
966        e = self._xpath('/KeePassFile/Meta/MasterKeyChanged', first=True)
967        if e is not None:
968            return self._decode_time(e.text)

datetime.datetime: get or set UTC time of last credential change

credchange_required
975    @property
976    def credchange_required(self):
977        """`bool`: Check if credential change is required"""
978        change_date = self.credchange_date
979        if change_date is None or self.credchange_required_days == -1:
980            return False
981        now_date = datetime.now(timezone.utc)
982        return (now_date - change_date).days > self.credchange_required_days

bool: Check if credential change is required

class Entry(pykeepass.baseelement.BaseElement):
 26class Entry(BaseElement):
 27
 28    def __init__(self, title=None, username=None, password=None, url=None,
 29                 notes=None, otp=None, tags=None, expires=False, expiry_time=None,
 30                 icon=None, autotype_sequence=None, autotype_enabled=True, autotype_window=None,
 31                 element=None, kp=None):
 32
 33        self._kp = kp
 34
 35        if element is None:
 36            super().__init__(
 37                element=Element('Entry'),
 38                kp=kp,
 39                expires=expires,
 40                expiry_time=expiry_time,
 41                icon=icon
 42            )
 43            self._element.append(E.String(E.Key('Title'), E.Value(title)))
 44            self._element.append(E.String(E.Key('UserName'), E.Value(username)))
 45            self._element.append(
 46                E.String(E.Key('Password'), E.Value(password, Protected="True"))
 47            )
 48            if url:
 49                self._element.append(E.String(E.Key('URL'), E.Value(url)))
 50            if notes:
 51                self._element.append(E.String(E.Key('Notes'), E.Value(notes)))
 52            if otp:
 53                self._element.append(
 54                    E.String(E.Key('otp'), E.Value(otp, Protected="True"))
 55                )
 56            if tags:
 57                self._element.append(
 58                    E.Tags(';'.join(tags) if isinstance(tags, list) else tags)
 59                )
 60            self._element.append(
 61                E.AutoType(
 62                    E.Enabled(str(autotype_enabled)),
 63                    E.DataTransferObfuscation('0'),
 64                    E.DefaultSequence(str(autotype_sequence) if autotype_sequence else ''),
 65                    E.Association(
 66                        E.Window(str(autotype_window) if autotype_window else ''),
 67                        E.KeystrokeSequence('')
 68                    )
 69                )
 70            )
 71            # FIXME: include custom_properties in constructor
 72
 73        else:
 74            assert type(element) in [_Element, Element, ObjectifiedElement], \
 75                'The provided element is not an LXML Element, but a {}'.format(
 76                    type(element)
 77                )
 78            assert element.tag == 'Entry', 'The provided element is not an Entry '\
 79                'element, but a {}'.format(element.tag)
 80            self._element = element
 81
 82    def _get_string_field(self, key):
 83        """Get a string field from an entry
 84
 85        Args:
 86            key (`str`): name of field
 87
 88        Returns:
 89            `str` or `None`: field value
 90        """
 91
 92        field = self._xpath('String/Key[text()="{}"]/../Value'.format(key), history=True, first=True)
 93        if field is not None:
 94            return field.text
 95
 96    def _set_string_field(self, key, value, protected=None):
 97        """Create or overwrite a string field in an Entry
 98
 99        Args:
100            key (`str`): name of field
101            value (`str`): value of field
102            protected (`bool` or `None`): mark whether the field should be protected in memory
103                in other tools.  If `None`, value is either copied from existing field or field
104                is created with protected property unset.
105
106        Note: pykeepass does not support memory protection
107        """
108        field = self._xpath('String/Key[text()="{}"]/..'.format(key), history=True, first=True)
109
110        protected_str = None
111        if protected is None:
112            protected_field = self._xpath('String/Key[text()="{}"]/../Value'.format(key), first=True)
113            if protected_field is not None:
114                protected_str = protected_field.attrib.get("Protected")
115        else:
116            protected_str = str(protected)
117
118        if field is not None:
119            self._element.remove(field)
120
121        if protected_str is None:
122            self._element.append(E.String(E.Key(key), E.Value(value)))
123        else:
124            self._element.append(E.String(E.Key(key), E.Value(value, Protected=protected_str)))
125
126    def _get_string_field_keys(self, exclude_reserved=False):
127        results = [x.find('Key').text for x in self._element.findall('String')]
128        if exclude_reserved:
129            return [x for x in results if x not in reserved_keys]
130        else:
131            return results
132
133    @property
134    def index(self):
135        """`int`: index of a entry within a group"""
136        group = self.group._element
137        children = group.getchildren()
138        first_index = self.group._first_entry
139        index = children.index(self._element)
140        return index - first_index
141
142    def reindex(self, new_index):
143        """Move entry to a new index within a group
144        
145        Args:
146            new_index (`int`): new index for the entry starting at 0
147        """
148        group = self.group._element
149        first_index = self.group._first_entry
150        group.remove(self._element)
151        group.insert(new_index+first_index, self._element)
152
153    @property
154    def attachments(self):
155        """`list` of `Attachment`: attachments associated with entry"""
156        return self._kp.find_attachments(
157            element=self,
158            filename='.*',
159            regex=True,
160            recursive=False
161        )
162
163    def add_attachment(self, id, filename):
164        """Add attachment to entry
165
166        The existence of a binary with the given `id` is not checked
167
168        Args:
169            id (`int`): ID of attachment in database
170            filename (`str`): filename to assign to this attachment data
171
172        Returns:
173            `Attachment`
174        """
175        element = E.Binary(
176            E.Key(filename),
177            E.Value(Ref=str(id))
178        )
179        self._element.append(element)
180
181        return attachment.Attachment(element=element, kp=self._kp)
182
183    def delete_attachment(self, attachment):
184        """remove an attachment from entry.  Does not remove binary data"""
185        attachment.delete()
186
187    def deref(self, attribute):
188        """See `PyKeePass.deref`"""
189        return self._kp.deref(getattr(self, attribute))
190
191    @property
192    def title(self) -> str:
193        """get or set entry title"""
194        return self._get_string_field('Title')
195
196    @title.setter
197    def title(self, value):
198        return self._set_string_field('Title', value)
199
200    @property
201    def username(self):
202        """`str`: get or set entry username"""
203        return self._get_string_field('UserName')
204
205    @username.setter
206    def username(self, value):
207        return self._set_string_field('UserName', value)
208
209    @property
210    def password(self):
211        """`str`: get or set entry password"""
212        return self._get_string_field('Password')
213
214    @password.setter
215    def password(self, value):
216        if self.password:
217            return self._set_string_field('Password', value)
218        else:
219            return self._set_string_field('Password', value, True)
220
221    @property
222    def url(self):
223        """str: get or set entry URL"""
224        return self._get_string_field('URL')
225
226    @url.setter
227    def url(self, value):
228        return self._set_string_field('URL', value)
229
230    @property
231    def notes(self):
232        """`str`: get or set entry notes"""
233        return self._get_string_field('Notes')
234
235    @notes.setter
236    def notes(self, value):
237        return self._set_string_field('Notes', value)
238
239    @property
240    def icon(self):
241        """`str`: get or set entry icon. See `icons`"""
242        return self._get_subelement_text('IconID')
243
244    @icon.setter
245    def icon(self, value):
246        return self._set_subelement_text('IconID', value)
247
248    @property
249    def tags(self) -> list[str]:
250        """`str`: get or set entry tags"""
251        val = self._get_subelement_text('Tags')
252        return val.replace(',', ';').split(';') if val else []
253
254    @tags.setter
255    def tags(self, value: str|list, sep=';'):
256        # Accept both str or list
257        v = sep.join(value if isinstance(value, list) else [value])
258        return self._set_subelement_text('Tags', v)
259
260    @property
261    def otp(self):
262        """`str`: get or set entry OTP text. (defacto standard)"""
263        return self._get_string_field('otp')
264
265    @otp.setter
266    def otp(self, value):
267        if self.otp:
268            return self._set_string_field('otp', value)
269        else:
270            return self._set_string_field('otp', value, True)
271
272    @property
273    def history(self):
274        """`list` of `HistoryEntry`: get entry history"""
275        if self._element.find('History') is not None:
276            return [HistoryEntry(element=x, kp=self._kp) for x in self._element.find('History').findall('Entry')]
277        else:
278            return []
279
280    @history.setter
281    def history(self, value):
282        raise NotImplementedError()
283
284    @property
285    def autotype_enabled(self):
286        """bool: get or set autotype enabled state.  Determines whether `autotype_sequence` should be used"""
287        enabled = self._element.find('AutoType/Enabled')
288        if enabled.text is not None:
289            return enabled.text == 'True'
290
291    @autotype_enabled.setter
292    def autotype_enabled(self, value):
293        enabled = self._element.find('AutoType/Enabled')
294        if value is not None:
295            enabled.text = str(value)
296        else:
297            enabled.text = None
298
299    @property
300    def autotype_sequence(self):
301        """str: get or set [autotype string](https://keepass.info/help/base/autotype.html)"""
302        sequence = self._element.find('AutoType/DefaultSequence')
303        if sequence is None or sequence.text == '':
304            return None
305        return sequence.text
306
307    @autotype_sequence.setter
308    def autotype_sequence(self, value):
309        self._element.find('AutoType/DefaultSequence').text = value
310
311    @property
312    def autotype_window(self):
313        """`str`: get or set [autotype target window filter](https://keepass.info/help/base/autotype.html#autowindows)"""
314        sequence = self._element.find('AutoType/Association/Window')
315        if sequence is None or sequence.text == '':
316            return None
317        return sequence.text
318
319    @autotype_window.setter
320    def autotype_window(self, value):
321        self._element.find('AutoType/Association/Window').text = value
322
323    @property
324    def is_a_history_entry(self):
325        """`bool`: check if entry is History entry"""
326        parent = self._element.getparent()
327        if parent is not None:
328            return parent.tag == 'History'
329        return False
330
331    @property
332    def path(self):
333        """`list` of (`str` or `None`): Path of entry.  List contains all parent group names
334        ending with entry title. May contain `None` for unnamed/untitled groups/entries."""
335
336        # The root group is an orphan
337        if self.parentgroup is None:
338            return None
339        p = self.parentgroup
340        path = [self.title]
341        while p is not None and not p.is_root_group:
342            if p.name is not None:  # dont make the root group appear
343                path.insert(0, p.name)
344            p = p.parentgroup
345        return path
346
347    def set_custom_property(self, key, value, protect=False):
348        assert key not in reserved_keys, '{} is a reserved key'.format(key)
349        return self._set_string_field(key, value, protect)
350
351    def get_custom_property(self, key):
352        assert key not in reserved_keys, '{} is a reserved key'.format(key)
353        return self._get_string_field(key)
354
355    def delete_custom_property(self, key):
356        if key not in self._get_string_field_keys(exclude_reserved=True):
357            raise AttributeError('No such key: {}'.format(key))
358        prop = self._xpath('String/Key[text()="{}"]/..'.format(key), first=True)
359        if prop is None:
360            raise AttributeError('Could not find property element')
361        self._element.remove(prop)
362
363    def is_custom_property_protected(self, key):
364        """Whether a custom property is protected.
365
366        Return False if the entry does not have a custom property with the
367        specified key.
368
369        Args:
370            key (`str`): key of the custom property to check.
371
372        Returns:
373            `bool`: Whether the custom property is protected.
374
375        """
376        assert key not in reserved_keys, '{} is a reserved key'.format(key)
377        return self._is_property_protected(key)
378
379    def _is_property_protected(self, key):
380        """Whether a property is protected."""
381        field = self._xpath('String/Key[text()="{}"]/../Value'.format(key), first=True)
382        if field is not None:
383            return field.attrib.get("Protected", "False") == "True"
384        return False
385
386    @property
387    def custom_properties(self):
388        keys = self._get_string_field_keys(exclude_reserved=True)
389        props = {}
390        for k in keys:
391            props[k] = self._get_string_field(k)
392        return props
393
394    def ref(self, attribute):
395        """Create reference to an attribute of this element.
396
397        Args:
398            attribute (`str`): one of 'title', 'username', 'password', 'url', 'notes', or 'uuid'
399
400        Returns:
401            `str`: [field reference][fieldref] to this field of this entry
402
403        [fieldref]: https://keepass.info/help/base/fieldrefs.html
404        """
405        attribute_to_field = {
406            'title': 'T',
407            'username': 'U',
408            'password': 'P',
409            'url': 'A',
410            'notes': 'N',
411            'uuid': 'I',
412        }
413        return '{{REF:{}@I:{}}}'.format(attribute_to_field[attribute], self.uuid.hex.upper())
414
415    def save_history(self):
416        """
417        Save the entry in its history.  History is not created unless this function is
418        explicitly called.
419        """
420        archive = deepcopy(self._element)
421        hist = archive.find('History')
422        if hist is not None:
423            archive.remove(hist)
424            self._element.find('History').append(archive)
425        else:
426            history = Element('History')
427            history.append(archive)
428            self._element.append(history)
429
430    def delete_history(self, history_entry=None, all=False):
431        """
432        Delete entries from history
433
434        Args:
435            history_entry (`Entry`): history item to delete
436            all (`bool`): delete all entries from history.  Default is False
437        """
438
439        if all:
440            self._element.remove(self._element.find('History'))
441        else:
442            self._element.find('History').remove(history_entry._element)
443
444    def __str__(self):
445        # filter out NoneTypes and join into string
446        pathstr = '/'.join('' if p is None else p for p in self.path)
447        return 'Entry: "{} ({})"'.format(pathstr, self.username)

Entry and Group inherit from this class

Entry( title=None, username=None, password=None, url=None, notes=None, otp=None, tags=None, expires=False, expiry_time=None, icon=None, autotype_sequence=None, autotype_enabled=True, autotype_window=None, element=None, kp=None)
28    def __init__(self, title=None, username=None, password=None, url=None,
29                 notes=None, otp=None, tags=None, expires=False, expiry_time=None,
30                 icon=None, autotype_sequence=None, autotype_enabled=True, autotype_window=None,
31                 element=None, kp=None):
32
33        self._kp = kp
34
35        if element is None:
36            super().__init__(
37                element=Element('Entry'),
38                kp=kp,
39                expires=expires,
40                expiry_time=expiry_time,
41                icon=icon
42            )
43            self._element.append(E.String(E.Key('Title'), E.Value(title)))
44            self._element.append(E.String(E.Key('UserName'), E.Value(username)))
45            self._element.append(
46                E.String(E.Key('Password'), E.Value(password, Protected="True"))
47            )
48            if url:
49                self._element.append(E.String(E.Key('URL'), E.Value(url)))
50            if notes:
51                self._element.append(E.String(E.Key('Notes'), E.Value(notes)))
52            if otp:
53                self._element.append(
54                    E.String(E.Key('otp'), E.Value(otp, Protected="True"))
55                )
56            if tags:
57                self._element.append(
58                    E.Tags(';'.join(tags) if isinstance(tags, list) else tags)
59                )
60            self._element.append(
61                E.AutoType(
62                    E.Enabled(str(autotype_enabled)),
63                    E.DataTransferObfuscation('0'),
64                    E.DefaultSequence(str(autotype_sequence) if autotype_sequence else ''),
65                    E.Association(
66                        E.Window(str(autotype_window) if autotype_window else ''),
67                        E.KeystrokeSequence('')
68                    )
69                )
70            )
71            # FIXME: include custom_properties in constructor
72
73        else:
74            assert type(element) in [_Element, Element, ObjectifiedElement], \
75                'The provided element is not an LXML Element, but a {}'.format(
76                    type(element)
77                )
78            assert element.tag == 'Entry', 'The provided element is not an Entry '\
79                'element, but a {}'.format(element.tag)
80            self._element = element
index
133    @property
134    def index(self):
135        """`int`: index of a entry within a group"""
136        group = self.group._element
137        children = group.getchildren()
138        first_index = self.group._first_entry
139        index = children.index(self._element)
140        return index - first_index

int: index of a entry within a group

def reindex(self, new_index):
142    def reindex(self, new_index):
143        """Move entry to a new index within a group
144        
145        Args:
146            new_index (`int`): new index for the entry starting at 0
147        """
148        group = self.group._element
149        first_index = self.group._first_entry
150        group.remove(self._element)
151        group.insert(new_index+first_index, self._element)

Move entry to a new index within a group

Arguments:
  • new_index (int): new index for the entry starting at 0
attachments
153    @property
154    def attachments(self):
155        """`list` of `Attachment`: attachments associated with entry"""
156        return self._kp.find_attachments(
157            element=self,
158            filename='.*',
159            regex=True,
160            recursive=False
161        )

list of Attachment: attachments associated with entry

def add_attachment(self, id, filename):
163    def add_attachment(self, id, filename):
164        """Add attachment to entry
165
166        The existence of a binary with the given `id` is not checked
167
168        Args:
169            id (`int`): ID of attachment in database
170            filename (`str`): filename to assign to this attachment data
171
172        Returns:
173            `Attachment`
174        """
175        element = E.Binary(
176            E.Key(filename),
177            E.Value(Ref=str(id))
178        )
179        self._element.append(element)
180
181        return attachment.Attachment(element=element, kp=self._kp)

Add attachment to entry

The existence of a binary with the given id is not checked

Arguments:
  • id (int): ID of attachment in database
  • filename (str): filename to assign to this attachment data
Returns:

Attachment

def delete_attachment(self, attachment):
183    def delete_attachment(self, attachment):
184        """remove an attachment from entry.  Does not remove binary data"""
185        attachment.delete()

remove an attachment from entry. Does not remove binary data

def deref(self, attribute):
187    def deref(self, attribute):
188        """See `PyKeePass.deref`"""
189        return self._kp.deref(getattr(self, attribute))
title: str
191    @property
192    def title(self) -> str:
193        """get or set entry title"""
194        return self._get_string_field('Title')

get or set entry title

username
200    @property
201    def username(self):
202        """`str`: get or set entry username"""
203        return self._get_string_field('UserName')

str: get or set entry username

password
209    @property
210    def password(self):
211        """`str`: get or set entry password"""
212        return self._get_string_field('Password')

str: get or set entry password

url
221    @property
222    def url(self):
223        """str: get or set entry URL"""
224        return self._get_string_field('URL')

str: get or set entry URL

notes
230    @property
231    def notes(self):
232        """`str`: get or set entry notes"""
233        return self._get_string_field('Notes')

str: get or set entry notes

icon
239    @property
240    def icon(self):
241        """`str`: get or set entry icon. See `icons`"""
242        return self._get_subelement_text('IconID')

str: get or set entry icon. See icons

tags: list[str]
248    @property
249    def tags(self) -> list[str]:
250        """`str`: get or set entry tags"""
251        val = self._get_subelement_text('Tags')
252        return val.replace(',', ';').split(';') if val else []

str: get or set entry tags

otp
260    @property
261    def otp(self):
262        """`str`: get or set entry OTP text. (defacto standard)"""
263        return self._get_string_field('otp')

str: get or set entry OTP text. (defacto standard)

history
272    @property
273    def history(self):
274        """`list` of `HistoryEntry`: get entry history"""
275        if self._element.find('History') is not None:
276            return [HistoryEntry(element=x, kp=self._kp) for x in self._element.find('History').findall('Entry')]
277        else:
278            return []

list of HistoryEntry: get entry history

autotype_enabled
284    @property
285    def autotype_enabled(self):
286        """bool: get or set autotype enabled state.  Determines whether `autotype_sequence` should be used"""
287        enabled = self._element.find('AutoType/Enabled')
288        if enabled.text is not None:
289            return enabled.text == 'True'

bool: get or set autotype enabled state. Determines whether autotype_sequence should be used

autotype_sequence
299    @property
300    def autotype_sequence(self):
301        """str: get or set [autotype string](https://keepass.info/help/base/autotype.html)"""
302        sequence = self._element.find('AutoType/DefaultSequence')
303        if sequence is None or sequence.text == '':
304            return None
305        return sequence.text

str: get or set autotype string

autotype_window
311    @property
312    def autotype_window(self):
313        """`str`: get or set [autotype target window filter](https://keepass.info/help/base/autotype.html#autowindows)"""
314        sequence = self._element.find('AutoType/Association/Window')
315        if sequence is None or sequence.text == '':
316            return None
317        return sequence.text
is_a_history_entry
323    @property
324    def is_a_history_entry(self):
325        """`bool`: check if entry is History entry"""
326        parent = self._element.getparent()
327        if parent is not None:
328            return parent.tag == 'History'
329        return False

bool: check if entry is History entry

path
331    @property
332    def path(self):
333        """`list` of (`str` or `None`): Path of entry.  List contains all parent group names
334        ending with entry title. May contain `None` for unnamed/untitled groups/entries."""
335
336        # The root group is an orphan
337        if self.parentgroup is None:
338            return None
339        p = self.parentgroup
340        path = [self.title]
341        while p is not None and not p.is_root_group:
342            if p.name is not None:  # dont make the root group appear
343                path.insert(0, p.name)
344            p = p.parentgroup
345        return path

list of (str or None): Path of entry. List contains all parent group names ending with entry title. May contain None for unnamed/untitled groups/entries.

def set_custom_property(self, key, value, protect=False):
347    def set_custom_property(self, key, value, protect=False):
348        assert key not in reserved_keys, '{} is a reserved key'.format(key)
349        return self._set_string_field(key, value, protect)
def get_custom_property(self, key):
351    def get_custom_property(self, key):
352        assert key not in reserved_keys, '{} is a reserved key'.format(key)
353        return self._get_string_field(key)
def delete_custom_property(self, key):
355    def delete_custom_property(self, key):
356        if key not in self._get_string_field_keys(exclude_reserved=True):
357            raise AttributeError('No such key: {}'.format(key))
358        prop = self._xpath('String/Key[text()="{}"]/..'.format(key), first=True)
359        if prop is None:
360            raise AttributeError('Could not find property element')
361        self._element.remove(prop)
def is_custom_property_protected(self, key):
363    def is_custom_property_protected(self, key):
364        """Whether a custom property is protected.
365
366        Return False if the entry does not have a custom property with the
367        specified key.
368
369        Args:
370            key (`str`): key of the custom property to check.
371
372        Returns:
373            `bool`: Whether the custom property is protected.
374
375        """
376        assert key not in reserved_keys, '{} is a reserved key'.format(key)
377        return self._is_property_protected(key)

Whether a custom property is protected.

Return False if the entry does not have a custom property with the specified key.

Arguments:
  • key (str): key of the custom property to check.
Returns:

bool: Whether the custom property is protected.

custom_properties
386    @property
387    def custom_properties(self):
388        keys = self._get_string_field_keys(exclude_reserved=True)
389        props = {}
390        for k in keys:
391            props[k] = self._get_string_field(k)
392        return props
def ref(self, attribute):
394    def ref(self, attribute):
395        """Create reference to an attribute of this element.
396
397        Args:
398            attribute (`str`): one of 'title', 'username', 'password', 'url', 'notes', or 'uuid'
399
400        Returns:
401            `str`: [field reference][fieldref] to this field of this entry
402
403        [fieldref]: https://keepass.info/help/base/fieldrefs.html
404        """
405        attribute_to_field = {
406            'title': 'T',
407            'username': 'U',
408            'password': 'P',
409            'url': 'A',
410            'notes': 'N',
411            'uuid': 'I',
412        }
413        return '{{REF:{}@I:{}}}'.format(attribute_to_field[attribute], self.uuid.hex.upper())

Create reference to an attribute of this element.

Arguments:
  • attribute (str): one of 'title', 'username', 'password', 'url', 'notes', or 'uuid'
Returns:

str: field reference to this field of this entry

def save_history(self):
415    def save_history(self):
416        """
417        Save the entry in its history.  History is not created unless this function is
418        explicitly called.
419        """
420        archive = deepcopy(self._element)
421        hist = archive.find('History')
422        if hist is not None:
423            archive.remove(hist)
424            self._element.find('History').append(archive)
425        else:
426            history = Element('History')
427            history.append(archive)
428            self._element.append(history)

Save the entry in its history. History is not created unless this function is explicitly called.

def delete_history(self, history_entry=None, all=False):
430    def delete_history(self, history_entry=None, all=False):
431        """
432        Delete entries from history
433
434        Args:
435            history_entry (`Entry`): history item to delete
436            all (`bool`): delete all entries from history.  Default is False
437        """
438
439        if all:
440            self._element.remove(self._element.find('History'))
441        else:
442            self._element.find('History').remove(history_entry._element)

Delete entries from history

Arguments:
  • history_entry (Entry): history item to delete
  • all (bool): delete all entries from history. Default is False
class Group(pykeepass.baseelement.BaseElement):
 10class Group(BaseElement):
 11
 12    def __init__(self, name=None, element=None, icon=None, notes=None,
 13                 kp=None, expires=None, expiry_time=None):
 14
 15        self._kp = kp
 16
 17        if element is None:
 18            super().__init__(
 19                element=Element('Group'),
 20                kp=kp,
 21                expires=expires,
 22                expiry_time=expiry_time,
 23                icon=icon
 24            )
 25            self._element.append(E.Name(name))
 26            if notes:
 27                self._element.append(E.Notes(notes))
 28
 29        else:
 30            assert type(element) in [_Element, Element, ObjectifiedElement], \
 31                'The provided element is not an LXML Element, but {}'.format(
 32                    type(element)
 33                )
 34            assert element.tag == 'Group', 'The provided element is not a Group '\
 35                'element, but a {}'.format(element.tag)
 36            self._element = element
 37
 38    @property
 39    def _first_entry(self):
 40        children = self._element.getchildren()
 41        first_element = next(e for e in children if e.tag == "Entry")
 42        return children.index(first_element)
 43
 44    @property
 45    def name(self):
 46        """`str`: get or set group name"""
 47        return self._get_subelement_text('Name')
 48
 49    @name.setter
 50    def name(self, value):
 51        return self._set_subelement_text('Name', value)
 52
 53    @property
 54    def notes(self):
 55        """`str`: get or set group notes"""
 56        return self._get_subelement_text('Notes')
 57
 58    @notes.setter
 59    def notes(self, value):
 60        return self._set_subelement_text('Notes', value)
 61
 62    @property
 63    def entries(self):
 64        """`list` of `Entry`: get list of entries in this group"""
 65        return [Entry(element=x, kp=self._kp) for x in self._element.findall('Entry')]
 66
 67    @property
 68    def subgroups(self):
 69        """`list` of `Group`: get list of groups in this group"""
 70        return [Group(element=x, kp=self._kp) for x in self._element.findall('Group')]
 71
 72    @property
 73    def is_root_group(self):
 74        """`bool`: return True if this is the database root"""
 75        return self._element.getparent().tag == 'Root'
 76
 77    @property
 78    def path(self):
 79        """`list` of (`str` or `None`): names of all parent groups, not including root"""
 80        # The root group is an orphan
 81        if self.is_root_group or self.parentgroup is None:
 82            return []
 83        p = self.parentgroup
 84        path = [self.name]
 85        while p is not None and not p.is_root_group:
 86            if p.name is not None:  # dont make the root group appear
 87                path.insert(0, p.name)
 88            p = p.parentgroup
 89        return path
 90
 91    def append(self, entries):
 92        """Add copy of an entry to this group
 93
 94        Args:
 95            entries (`Entry` or `list` of `Entry`)
 96        """
 97        # FIXME: check if `entries` is iterable instead of list
 98        if isinstance(entries, list):
 99            for e in entries:
100                self._element.append(e._element)
101        else:
102            self._element.append(entries._element)
103
104    def __str__(self):
105        # filter out NoneTypes and join into string
106        pathstr = '/'.join('' if p is None else p for p in self.path)
107        return 'Group: "{}"'.format(pathstr)

Entry and Group inherit from this class

Group( name=None, element=None, icon=None, notes=None, kp=None, expires=None, expiry_time=None)
12    def __init__(self, name=None, element=None, icon=None, notes=None,
13                 kp=None, expires=None, expiry_time=None):
14
15        self._kp = kp
16
17        if element is None:
18            super().__init__(
19                element=Element('Group'),
20                kp=kp,
21                expires=expires,
22                expiry_time=expiry_time,
23                icon=icon
24            )
25            self._element.append(E.Name(name))
26            if notes:
27                self._element.append(E.Notes(notes))
28
29        else:
30            assert type(element) in [_Element, Element, ObjectifiedElement], \
31                'The provided element is not an LXML Element, but {}'.format(
32                    type(element)
33                )
34            assert element.tag == 'Group', 'The provided element is not a Group '\
35                'element, but a {}'.format(element.tag)
36            self._element = element
name
44    @property
45    def name(self):
46        """`str`: get or set group name"""
47        return self._get_subelement_text('Name')

str: get or set group name

notes
53    @property
54    def notes(self):
55        """`str`: get or set group notes"""
56        return self._get_subelement_text('Notes')

str: get or set group notes

entries
62    @property
63    def entries(self):
64        """`list` of `Entry`: get list of entries in this group"""
65        return [Entry(element=x, kp=self._kp) for x in self._element.findall('Entry')]

list of Entry: get list of entries in this group

subgroups
67    @property
68    def subgroups(self):
69        """`list` of `Group`: get list of groups in this group"""
70        return [Group(element=x, kp=self._kp) for x in self._element.findall('Group')]

list of Group: get list of groups in this group

is_root_group
72    @property
73    def is_root_group(self):
74        """`bool`: return True if this is the database root"""
75        return self._element.getparent().tag == 'Root'

bool: return True if this is the database root

path
77    @property
78    def path(self):
79        """`list` of (`str` or `None`): names of all parent groups, not including root"""
80        # The root group is an orphan
81        if self.is_root_group or self.parentgroup is None:
82            return []
83        p = self.parentgroup
84        path = [self.name]
85        while p is not None and not p.is_root_group:
86            if p.name is not None:  # dont make the root group appear
87                path.insert(0, p.name)
88            p = p.parentgroup
89        return path

list of (str or None): names of all parent groups, not including root

def append(self, entries):
 91    def append(self, entries):
 92        """Add copy of an entry to this group
 93
 94        Args:
 95            entries (`Entry` or `list` of `Entry`)
 96        """
 97        # FIXME: check if `entries` is iterable instead of list
 98        if isinstance(entries, list):
 99            for e in entries:
100                self._element.append(e._element)
101        else:
102            self._element.append(entries._element)

Add copy of an entry to this group

Arguments:
class Attachment:
 6class Attachment:
 7    """Binary data attached to an `Entry`.
 8
 9    *Binary* refers to the bytes of the attached data
10    (stored at the root level of the database), while *attachment* is a
11    reference to a binary (stored in an entry).  A binary can be referenced
12    by none, one or many attachments.
13    A piece of binary data may be attached to multiple entries
14
15    """
16    def __init__(self, element=None, kp=None, id=None, filename=None):
17        self._element = element
18        self._kp = kp
19
20    def __repr__(self):
21        return "Attachment: '{}' -> {}".format(self.filename, self.id)
22
23    @property
24    def id(self):
25        """`str`: get or set id of binary the attachment points to"""
26        return int(self._element.find('Value').attrib['Ref'])
27
28    @id.setter
29    def id(self, id):
30        self._element.find('Value').attrib['Ref'] = str(id)
31
32    @property
33    def filename(self):
34        """`str`: get or set filename string"""
35        return self._element.find('Key').text
36
37    @filename.setter
38    def filename(self, filename):
39        self._element.find('Key').text = filename
40
41    @property
42    def entry(self):
43        """`Entry`: entry this attachment is associated with"""
44        ancestor = self._element.getparent()
45        return entry.Entry(element=ancestor, kp=self._kp)
46
47    @property
48    def binary(self):
49        """`bytes`: binary data this attachment points to"""
50        try:
51            return self._kp.binaries[self.id]
52        except IndexError:
53            raise BinaryError('No such binary with id {}'.format(self.id))
54
55    data = binary
56
57    def delete(self):
58        """delete this attachment"""
59        self._element.getparent().remove(self._element)

Binary data attached to an Entry.

Binary refers to the bytes of the attached data (stored at the root level of the database), while attachment is a reference to a binary (stored in an entry). A binary can be referenced by none, one or many attachments. A piece of binary data may be attached to multiple entries

Attachment(element=None, kp=None, id=None, filename=None)
16    def __init__(self, element=None, kp=None, id=None, filename=None):
17        self._element = element
18        self._kp = kp
id
23    @property
24    def id(self):
25        """`str`: get or set id of binary the attachment points to"""
26        return int(self._element.find('Value').attrib['Ref'])

str: get or set id of binary the attachment points to

filename
32    @property
33    def filename(self):
34        """`str`: get or set filename string"""
35        return self._element.find('Key').text

str: get or set filename string

entry
41    @property
42    def entry(self):
43        """`Entry`: entry this attachment is associated with"""
44        ancestor = self._element.getparent()
45        return entry.Entry(element=ancestor, kp=self._kp)

Entry: entry this attachment is associated with

binary
47    @property
48    def binary(self):
49        """`bytes`: binary data this attachment points to"""
50        try:
51            return self._kp.binaries[self.id]
52        except IndexError:
53            raise BinaryError('No such binary with id {}'.format(self.id))

bytes: binary data this attachment points to

data
47    @property
48    def binary(self):
49        """`bytes`: binary data this attachment points to"""
50        try:
51            return self._kp.binaries[self.id]
52        except IndexError:
53            raise BinaryError('No such binary with id {}'.format(self.id))

bytes: binary data this attachment points to

def delete(self):
57    def delete(self):
58        """delete this attachment"""
59        self._element.getparent().remove(self._element)

delete this attachment

icons = namespace(Key='0', World='1', Warning='2', NetworkServer='3', MarkedDirectory='4', UserCommunication='5', Parts='6', Notepad='7', WorldSocket='8', Identity='9', PaperReady='10', Digicam='11', IRCommunication='12', MultiKeys='13', Energy='14', Scanner='15', WorldStar='16', CDRom='17', Monitor='18', EMail='19', Configuration='20', ClipboardReady='21', PaperNew='22', Screen='23', EnergyCareful='24', EMailBox='25', Disk='26', Drive='27', PaperQ='28', TerminalEncrypted='29', Console='30', Printer='31', ProgramIcons='32', Run='33', Settings='34', WorldComputer='35', Archive='36', Homebanking='37', DriveWindows='38', Clock='39', EMailSearch='40', PaperFlag='41', Memory='42', TrashBin='43', Note='44', Expired='45', Info='46', Package='47', Folder='48', FolderOpen='49', FolderPackage='50', LockOpen='51', PaperLocked='52', Checked='53', Pen='54', Thumbnail='55', Book='56', List='57', UserKey='58', Tool='59', Home='60', Star='61', Tux='62', Feather='63', Apple='64', Wiki='65', Money='66', Certificate='67', BlackBerry='68', KEY='0', GLOBE='1', WARNING_SIGN='2', SERVER='3', PINNED_NOTE='4', SPEECH_BUBBLE='5', SQUARES='6', HANDWRITTEN_NOTE='7', GLOBE_PLUG='8', BUSINESS_CARD='9', GREEN_AND_WHITE_THINGY='10', CAMERA='11', INFRARED='12', KEYS='13', POWER_PLUG='14', FLATBED_SCANNER='15', GLOBE_STAR='16', CD_ROM='17', MONITOR='18', ENVELOPE='19', GEAR='20', CHECKLIST='21', NOTEPAD='22', DESKTOP='23', POWER_FLASH='24', FOLDER_ENVELOPE='25', FLOPPY_DISK='26', SERVER_2='27', GREEN_DOT='28', MONITOR_KEY='29', SHELL='30', PRINTER='31', DASHBOARD='32', CROATIA='33', WRENCH='34', PC_INTERNET='35', ZIP_FILE='36', PERCENT_SIGN='37', SAMBA_SHARE='38', CLOCK='39', SEARCH='40', USED_TAMPON='41', MEMORY_STICK='42', RECYCLE_BIN='43', POST_IT='44', RED_CROSS='45', INFO_SIGN='46', CARDBOARD='47', FOLDER='48', FOLDER_OPEN='49', FOLDER_CUBE='50', LOCK_OPEN='51', LOCK_CLOSED='52', GREEN_CHECKMARK='53', FEATHER_PEN='54', POLAROID_PICTURE='55', BOOK_OPENED='56', UI='57', MANAGER='58', HAMMER='59', HOUSE='60', STAR='61', PENGUIN='62', FEATHER='63', APPLE='64', WIKIPEDIA='65', DOLLAR_SIGN='66', CERTIFICATE='67', SMARTPHONE='68')
def create_database(filename, password=None, keyfile=None, transformed_key=None):
1033def create_database(
1034        filename, password=None, keyfile=None, transformed_key=None
1035):
1036    """
1037    Create a new database at ``filename`` with supplied credentials.
1038
1039    Args:
1040        filename (`str`, optional): path to database or stream object.
1041            If None, the path given when the database was opened is used.
1042        password (`str`, optional): database password.  If None,
1043            database is assumed to have no password
1044        keyfile (`str`, optional): path to keyfile.  If None,
1045            database is assumed to have no keyfile
1046        transformed_key (`bytes`, optional): precomputed transformed
1047            key.
1048
1049    Returns:
1050        `PyKeePass`
1051    """
1052    keepass_instance = PyKeePass(
1053        BLANK_DATABASE_LOCATION, BLANK_DATABASE_PASSWORD
1054    )
1055
1056    keepass_instance.filename = filename
1057    keepass_instance.password = password
1058    keepass_instance.keyfile = keyfile
1059
1060    keepass_instance.save(transformed_key)
1061    return keepass_instance

Create a new database at filename with supplied credentials.

Arguments:
  • filename (str, optional): path to database or stream object. If None, the path given when the database was opened is used.
  • password (str, optional): database password. If None, database is assumed to have no password
  • keyfile (str, optional): path to keyfile. If None, database is assumed to have no keyfile
  • transformed_key (bytes, optional): precomputed transformed key.
Returns:

PyKeePass