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"]
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. SetFalse
to access outer header information without decrypting database.
Raises:
CredentialsError
: raised when password/keyfile or transformed key are wrongHeaderChecksumError
: raised when checksum in database header is is wrong. e.g. database tampering or file corruptionPayloadChecksumError
: raised when payload blocks checksum is wrong, e.g. corruption during database saving
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.
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
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.
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).
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'.
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'
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
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.
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
242 @property 243 def tree(self): 244 """`lxml.etree._ElementTree`: database XML payload""" 245 return self.payload.xml
lxml.etree._ElementTree
: database XML payload
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
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
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
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
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
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
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
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
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
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. (defaultFalse
). - 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. (defaultFalse
)
Returns:
list
ofGroup
,Entry
,Attachment
, orlxml.etree.Element
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 (defaultFalse
) - recursive (
bool
): do a recursive search of all groups/subgroups - path (
list
ofstr
): do group search starting from path - group (
Group
): search underneath group - uuid (
uuid.UUID
): group UUID - regex (
bool
): whetherstr
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 ofGroup
or[]
if there are no matches - if
first=True
, the function returns the firstGroup
match, orNone
if there are no matches
Returns:
list
ofGroup
iffirst=False
or (Group
orNone
) iffirst=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"]
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
536 def move_group(self, group, destination_group): 537 """Move a group""" 538 destination_group.append(group)
Move a 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
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
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
orNone
), optional): full path to an entry (eg.['foobar_group', 'foobar_entry']
). This impliesfirst=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
ofstr
): 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
orNone
, optional): search under this group - first (
bool
, optional): return first match orNone
if no matches. Otherwise return list ofEntry
matches. (defaultFalse
) - history (
bool
): include history entries in results. (defaultFalse
) - recursive (
bool
): search recursively - regex (
bool
): interpret search strings given above as XSLT style regexes - flags (
str
): regex search flags
Returns:
list
ofEntry
iffirst=False
or (Entry
orNone
) iffirst=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)"
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
, orNone
): title of new entry - username (
str
orNone
): username of new entry - password (
str
orNone
): password of new entry - url (
str
orNone
): URL of new entry - notes (
str
orNone
): notes of new entry - expiry_time (
datetime.datetime
): time of entry expiration - tags (
list
ofstr
orNone
): entry tags - otp (
str
orNone
): OTP code of object - icon (
str
, optional): icon name fromicons
- force_creation (
bool
): create entry even if one with identical title exists in this group (defaultFalse
)
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
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
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
orNone
): attachment ID to match - filename (
str
orNone
): filename to match - element (
Entry
orGroup
orNone
): entry or group to search under - recursive (
bool
): search recursively (defaultTrue
) - regex (
bool
): whetherstr
search arguments contain [XSLT style][XSLT style] regular expression - flags (
str
): XPath [flags][flags] - history (
bool
): search under history entries. (defaultFalse
) - first (
bool
): If True, function returns first result or None. If False, function returns list of matches or empty list. (defaultFalse
).
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
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
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. (defaultTrue
). Applies only to KDBX3 - protected (
bool
): whether protected flag should be set. (defaultTrue
). Note Applies only to KDBX4
Returns:
id (
int
): ID of binary in database
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
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
orNone
if no match found
917 @property 918 def password(self): 919 """`str`: Get or set database password""" 920 return self._password
str
: Get or set database password
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
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
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)
int
: Days until password update should be recommended
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
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
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
bool
: Check if credential change is recommended
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
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
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
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
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
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:
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
187 def deref(self, attribute): 188 """See `PyKeePass.deref`""" 189 return self._kp.deref(getattr(self, attribute))
See PyKeePass.deref
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
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
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
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
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
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
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)
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
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
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
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
str
: get or set autotype target window filter
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
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.
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)
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.
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
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.
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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.