diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb5c6f0 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# pykeepass + + + + + + +This library allows you to write entries to a KeePass database. + +Come chat at [#pykeepass:matrix.org](https://matrix.to/#/%23pykeepass:matrix.org) on Matrix. + +# Installation + +``` bash +sudo apt install python3-lxml +pip install pykeepass +``` + +Quickstart +------- + +General database manipulation + +``` python +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 + +``` python +# 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 + +``` python +>>> 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 + +``` python +>>> 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 + +``` python +# 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)" diff --git a/README.rst b/README.rst deleted file mode 100644 index d29ebed..0000000 --- a/README.rst +++ /dev/null @@ -1,499 +0,0 @@ -pykeepass -============ - -.. image:: https://github.com/libkeepass/pykeepass/actions/workflows/ci.yaml/badge.svg - :target: https://github.com/libkeepass/pykeepass/actions/workflows/ci.yaml - -.. image:: https://readthedocs.org/projects/pykeepass/badge/?version=latest - :target: https://pykeepass.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -.. image:: https://img.shields.io/matrix/pykeepass:matrix.org.svg - :target: https://matrix.to/#/#pykeepass:matrix.org - -.. image:: https://img.shields.io/badge/irc-%23pykeepass-brightgreen - :target: https://webchat.freenode.net/?channels=pykeepass - -This library allows you to write entries to a KeePass database. - -Come chat at `#pykeepass:matrix.org`_ on Matrix. - -.. _#pykeepass\:matrix.org: https://matrix.to/#/%23pykeepass:matrix.org - -Installation ------------- - -.. code:: - - sudo apt install python3-lxml - pip install pykeepass - -Example -------- -.. code:: python - - from pykeepass import PyKeePass - - # load database - >>> kp = PyKeePass('db.kdbx', password='somePassw0rd') - - # 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 - >>> entry.password - 's3cure_p455w0rd' - - # 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() - - -.. - TODO: add `Entry` and `Group` sections to document attributes of each - -Finding Entries ---------------- - -**find_entries** (title=None, username=None, password=None, url=None, notes=None, otp=None, path=None, uuid=None, tags=None, string=None, group=None, recursive=True, regex=False, flags=None, history=False, first=False) - -Returns entries which match all provided parameters, where ``title``, ``username``, ``password``, ``url``, ``notes``, ``otp``, ``autotype_window`` and ``autotype_sequence`` are strings, ``path`` is a list, ``string`` is a dict, ``autotype_enabled`` is a boolean, ``uuid`` is a ``uuid.UUID`` and ``tags`` is a list of strings. This function has optional ``regex`` boolean and ``flags`` string arguments, which means to interpret search strings as `XSLT style`_ regular expressions with `flags`_. - -.. _XSLT style: https://www.xml.com/pub/a/2003/06/04/tr.html -.. _flags: https://www.w3.org/TR/xpath-functions/#flags - -The ``path`` list is a full path to an entry (ex. ``['foobar_group', 'foobar_entry']``). This implies ``first=True``. All other arguments are ignored when this is given. This is useful for handling user input. - -The ``string`` dict allows for searching custom string fields. ex. ``{'custom_field1': 'custom value', 'custom_field2': 'custom value'}`` - -The ``group`` argument determines what ``Group`` to search under, and the ``recursive`` boolean controls whether to search recursively. - -The ``history`` (default ``False``) boolean controls whether history entries should be included in the search results. - -The ``first`` (default ``False``) boolean controls whether to return the first matched item, or a list of matched items. - -* if ``first=False``, the function returns a list of ``Entry`` s or ``[]`` if there are no matches -* if ``first=True``, the function returns the first ``Entry`` match, or ``None`` if there are no matches - -**entries** - -a flattened list of all entries in the database - -.. code:: python - - >>> kp.entries - [Entry: "foo_entry (myusername)", Entry: "foobar_entry (myusername)", Entry: "social/gmail (myusername)", Entry: "social/facebook (myusername)"] - - >>> 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)" - - >>> entry.otp - otpauth://totp/test:lkj?secret=TEST%3D%3D%3D%3D&period=30&digits=6&issuer=test - - - -Finding Groups --------------- - -**find_groups** (name=None, path=None, uuid=None, notes=None, group=None, recursive=True, regex=False, flags=None, first=False) - -where ``name`` and ``notes`` are strings, ``path`` is a list, ``uuid`` is a ``uuid.UUID``. This function has optional ``regex`` boolean and ``flags`` string arguments, which means to interpret search strings as `XSLT style`_ regular expressions with `flags`_. - -.. _XSLT style: https://www.xml.com/pub/a/2003/06/04/tr.html -.. _flags: https://www.w3.org/TR/xpath-functions/#flags - -The ``path`` list is a full path to a group (ex. ``['foobar_group', 'sub_group']``). This implies ``first=True``. All other arguments are ignored when this is given. This is useful for handling user input. - -The ``group`` argument determines what ``Group`` to search under, and the ``recursive`` boolean controls whether to search recursively. - -The ``first`` (default ``False``) boolean controls whether to return the first matched item, or a list of matched items. - -* if ``first=False``, the function returns a list of ``Group`` s or ``[]`` if there are no matches -* if ``first=True``, the function returns the first ``Group`` match, or ``None`` if there are no matches - -**root_group** - -the ``Root`` group to the database - -**groups** - -a flattened list of all groups in the database - -.. code:: python - - >>> 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: "/" - - -Entry Functions and Properties ------------------------------- -**add_entry** (destination_group, title, username, password, url=None, notes=None, tags=None, expiry_time=None, icon=None, force_creation=False) - -**delete_entry** (entry) - -**trash_entry** (entry) - -move an entry to the recycle bin. The recycle bin is created if it does not exit. ``entry`` must be an empty Entry. - -**move_entry** (entry, destination_group) - -**atime** - -access time - -**ctime** - -creation time - -**mtime** - -modification time - -where ``destination_group`` is a ``Group`` instance. ``entry`` is an ``Entry`` instance. ``title``, ``username``, ``password``, ``url``, ``notes``, ``tags``, ``icon`` are strings. ``expiry_time`` is a ``datetime`` instance. - -If ``expiry_time`` is a naive datetime object (i.e. ``expiry_time.tzinfo`` is not set), the timezone is retrieved from ``dateutil.tz.gettz()``. - -.. code:: python - - # 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_history** () - -save the Entry in its history - - -Group Functions and Properties ------------------------------- -**add_group** (destination_group, group_name, icon=None, notes=None) - -**delete_group** (group) - -**trash_group** (group) - -move a group to the recycle bin. The recycle bin is created if it does not exit. ``group`` must be an empty Group. - -**empty_group** (group) - -delete all entries and subgroups of a group. ``group`` is an instance of ``Group``. - -**move_group** (group, destination_group) - -**atime** - -access time - -**ctime** - -creation time - -**mtime** - -modification time - -``destination_group`` and ``group`` are instances of ``Group``. ``group_name`` is a string - -.. code:: python - - # 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 ------------ - -In this section, *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. - -**add_binary** (data, compressed=True, protected=True) - -where ``data`` is bytes. Adds a blob of data to the database. The attachment reference must still be added to an entry (see below). ``compressed`` only applies to KDBX3 and ``protected`` only applies to KDBX4 (no effect if used on wrong database version). Returns id of attachment. - -**delete_binary** (id) - -where ``id`` is an int. Removes binary data from the database and deletes any attachments that reference it. Since attachments reference binaries by their positional index, attachments that reference binaries with id > ``id`` will automatically be decremented. - -**find_attachments** (id=None, filename=None, element=None, recursive=True, regex=False, flags=None, history=False, first=False) - -where ``id`` is an int, ``filename`` is a string, and element is an ``Entry`` or ``Group`` to search under. - -* if ``first=False``, the function returns a list of ``Attachment`` s or ``[]`` if there are no matches -* if ``first=True``, the function returns the first ``Attachment`` match, or ``None`` if there are no matches - -**binaries** - -list of bytestrings containing binary data. List index corresponds to attachment id - -**attachments** - -list containing all ``Attachment`` s in the database. - -**Entry.add_attachment** (id, filename) - -where ``id`` is an int and ``filename`` is a string. Creates a reference using the given filename to a database binary. The existence of a binary with the given id is not checked. Returns ``Attachment``. - -**Entry.delete_attachment** (attachment) - -where ``attachment`` is an ``Attachment``. Deletes a reference to a database binary. - -**Entry.attachments** - -list of ``Attachment`` s for this Entry. - -**Attachment.id** - -id of data that this attachment points to - -**Attachment.filename** - -string representing this attachment - -**Attachment.data** - -the data that this attachment points to. Raises ``BinaryError`` if data does not exist. - -**Attachment.entry** - -the entry that this attachment is attached to - -.. code:: python - - >>> 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** - -Credential Expiry ------------------ - -**credchange_date** - -datetime object with date of last credentials change - -**credchange_required** - -boolean whether database credentials have expired and are required to change - -**credchange_recommended** - -boolean whether database credentials have expired and are recommended to change - -**credchange_required_days** - -days after **credchange_date** that credential update is required - -**credchange_recommended_days** - -days after **credchange_date** that credential update is recommended - - -Miscellaneous -------------- -**read** (filename=None, password=None, keyfile=None, transformed_key=None, decrypt=False) - -where ``filename``, ``password``, and ``keyfile`` are strings ( ``filename`` and ``keyfile`` may also be file-like objects). ``filename`` is the path to the database, ``password`` is the master password string, and ``keyfile`` is the path to the database keyfile. At least one of ``password`` and ``keyfile`` is required. Alternatively, the derived key can be supplied directly through ``transformed_key``. ``decrypt`` tells whether the file should be decrypted or not. - -Can raise ``CredentialsError``, ``HeaderChecksumError``, or ``PayloadChecksumError``. - -**reload** () - -reload database from disk using previous credentials - -**save** (filename=None) - -where ``filename`` is the path of the file to save to (``filename`` may also be file-like object). If ``filename`` is not given, the path given in ``read`` will be used. - -**password** - -string containing database password. Can also be set. Use ``None`` for no password. - -**filename** - -string containing path to database. Can also be set - -**keyfile** - -string containing path to the database keyfile. Can also be set. Use ``None`` for no keyfile. - -**version** - -tuple containing database version. e.g. ``(3, 1)`` is a KDBX version 3.1 database. - -**encryption_algorithm** - -string containing algorithm used to encrypt database. Possible values are ``aes256``, ``chacha20``, and ``twofish``. - -**create_database** (filename, password=None, keyfile=None, transformed_key=None) - -create a new database at ``filename`` with supplied credentials. Returns ``PyKeePass`` object - -**tree** - -database lxml tree - -**xml** - -get database XML data as string - -**dump_xml** (filename) - -pretty print database XML to file - -TOTP -------- - -**Entry.otp** - -TOTP URI which can be passed to an OTP library to generate codes - -.. code:: python - - # 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 :code:`python tests/tests.py` or :code:`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)" diff --git a/pyproject.toml b/pyproject.toml index aecc4ba..520db09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "pykeepass" # setuptools normalizes semver '-' to '.' # avoid using hyphens: 1.2.3.post1 version = "4.1.0.post1" -readme = "README.rst" +readme = "README.md" description = "Python library to interact with keepass databases (supports KDBX3 and KDBX4)" authors = [ { name = "Philipp Schmitt", email = "philipp@schmitt.co" }, @@ -32,7 +32,7 @@ classifiers = [ ] [project.optional-dependencies] -test = ["pyotp"] +test = ["pyotp", "pdoc"] [project.urls] Homepage = "https://github.com/libkeepass/pykeepass"