././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/0000755000175000017500000000000014275747351013753 5ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/LICENSE0000644000175000017500000000274014224240652014745 0ustar00dmitrydmitryCopyright 2012-2018 Dmitry Shachnev All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the University nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/MANIFEST.in0000644000175000017500000000016014224240652015470 0ustar00dmitrydmitryinclude tests/*.py include LICENSE include README.rst include changelog include docs/*.rst include docs/conf.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/PKG-INFO0000644000175000017500000000757014275747351015061 0ustar00dmitrydmitryMetadata-Version: 2.1 Name: SecretStorage Version: 3.3.3 Summary: Python bindings to FreeDesktop.org Secret Service API Home-page: https://github.com/mitya57/secretstorage Author: Dmitry Shachnev Author-email: mitya57@gmail.com License: BSD 3-Clause License Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Security Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE .. image:: https://github.com/mitya57/secretstorage/workflows/tests/badge.svg :target: https://github.com/mitya57/secretstorage/actions :alt: GitHub Actions status .. image:: https://codecov.io/gh/mitya57/secretstorage/branch/master/graph/badge.svg :target: https://codecov.io/gh/mitya57/secretstorage :alt: Coverage status .. image:: https://readthedocs.org/projects/secretstorage/badge/?version=latest :target: https://secretstorage.readthedocs.io/en/latest/ :alt: ReadTheDocs status Module description ================== This module provides a way for securely storing passwords and other secrets. It uses D-Bus `Secret Service`_ API that is supported by GNOME Keyring, KWallet (since version 5.97) and KeePassXC. The main classes provided are ``secretstorage.Item``, representing a secret item (that has a *label*, a *secret* and some *attributes*) and ``secretstorage.Collection``, a place items are stored in. SecretStorage supports most of the functions provided by Secret Service, including creating and deleting items and collections, editing items, locking and unlocking collections (asynchronous unlocking is also supported). The documentation can be found on `secretstorage.readthedocs.io`_. .. _`Secret Service`: https://specifications.freedesktop.org/secret-service/ .. _`secretstorage.readthedocs.io`: https://secretstorage.readthedocs.io/en/latest/ Building the module =================== .. note:: SecretStorage 3.x supports Python 3.6 and newer versions. If you have an older version of Python, install SecretStorage 2.x:: pip install "SecretStorage < 3" SecretStorage requires these packages to work: * Jeepney_ * `python-cryptography`_ To build SecretStorage, use this command:: python3 setup.py build If you have Sphinx_ installed, you can also build the documentation:: python3 setup.py build_sphinx .. _Jeepney: https://pypi.org/project/jeepney/ .. _`python-cryptography`: https://pypi.org/project/cryptography/ .. _Sphinx: http://sphinx-doc.org/ Testing the module ================== First, make sure that you have the Secret Service daemon installed. The `GNOME Keyring`_ is the reference server-side implementation for the Secret Service specification. .. _`GNOME Keyring`: https://download.gnome.org/sources/gnome-keyring/ Then, start the daemon and unlock the ``default`` collection, if needed. The testsuite will fail to run if the ``default`` collection exists and is locked. If it does not exist, the testsuite can also use the temporary ``session`` collection, as provided by the GNOME Keyring. Then, run the Python unittest module:: python3 -m unittest discover -s tests If you want to run the tests in an isolated or headless environment, run this command in a D-Bus session:: dbus-run-session -- python3 -m unittest discover -s tests Get the code ============ SecretStorage is available under BSD license. The source code can be found on GitHub_. .. _GitHub: https://github.com/mitya57/secretstorage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658093232.0 SecretStorage-3.3.3/README.rst0000644000175000017500000000567514265077260015451 0ustar00dmitrydmitry.. image:: https://github.com/mitya57/secretstorage/workflows/tests/badge.svg :target: https://github.com/mitya57/secretstorage/actions :alt: GitHub Actions status .. image:: https://codecov.io/gh/mitya57/secretstorage/branch/master/graph/badge.svg :target: https://codecov.io/gh/mitya57/secretstorage :alt: Coverage status .. image:: https://readthedocs.org/projects/secretstorage/badge/?version=latest :target: https://secretstorage.readthedocs.io/en/latest/ :alt: ReadTheDocs status Module description ================== This module provides a way for securely storing passwords and other secrets. It uses D-Bus `Secret Service`_ API that is supported by GNOME Keyring, KWallet (since version 5.97) and KeePassXC. The main classes provided are ``secretstorage.Item``, representing a secret item (that has a *label*, a *secret* and some *attributes*) and ``secretstorage.Collection``, a place items are stored in. SecretStorage supports most of the functions provided by Secret Service, including creating and deleting items and collections, editing items, locking and unlocking collections (asynchronous unlocking is also supported). The documentation can be found on `secretstorage.readthedocs.io`_. .. _`Secret Service`: https://specifications.freedesktop.org/secret-service/ .. _`secretstorage.readthedocs.io`: https://secretstorage.readthedocs.io/en/latest/ Building the module =================== .. note:: SecretStorage 3.x supports Python 3.6 and newer versions. If you have an older version of Python, install SecretStorage 2.x:: pip install "SecretStorage < 3" SecretStorage requires these packages to work: * Jeepney_ * `python-cryptography`_ To build SecretStorage, use this command:: python3 setup.py build If you have Sphinx_ installed, you can also build the documentation:: python3 setup.py build_sphinx .. _Jeepney: https://pypi.org/project/jeepney/ .. _`python-cryptography`: https://pypi.org/project/cryptography/ .. _Sphinx: http://sphinx-doc.org/ Testing the module ================== First, make sure that you have the Secret Service daemon installed. The `GNOME Keyring`_ is the reference server-side implementation for the Secret Service specification. .. _`GNOME Keyring`: https://download.gnome.org/sources/gnome-keyring/ Then, start the daemon and unlock the ``default`` collection, if needed. The testsuite will fail to run if the ``default`` collection exists and is locked. If it does not exist, the testsuite can also use the temporary ``session`` collection, as provided by the GNOME Keyring. Then, run the Python unittest module:: python3 -m unittest discover -s tests If you want to run the tests in an isolated or headless environment, run this command in a D-Bus session:: dbus-run-session -- python3 -m unittest discover -s tests Get the code ============ SecretStorage is available under BSD license. The source code can be found on GitHub_. .. _GitHub: https://github.com/mitya57/secretstorage ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/SecretStorage.egg-info/0000755000175000017500000000000014275747351020217 5ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407528.0 SecretStorage-3.3.3/SecretStorage.egg-info/PKG-INFO0000644000175000017500000000757014275747350021324 0ustar00dmitrydmitryMetadata-Version: 2.1 Name: SecretStorage Version: 3.3.3 Summary: Python bindings to FreeDesktop.org Secret Service API Home-page: https://github.com/mitya57/secretstorage Author: Dmitry Shachnev Author-email: mitya57@gmail.com License: BSD 3-Clause License Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Security Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE .. image:: https://github.com/mitya57/secretstorage/workflows/tests/badge.svg :target: https://github.com/mitya57/secretstorage/actions :alt: GitHub Actions status .. image:: https://codecov.io/gh/mitya57/secretstorage/branch/master/graph/badge.svg :target: https://codecov.io/gh/mitya57/secretstorage :alt: Coverage status .. image:: https://readthedocs.org/projects/secretstorage/badge/?version=latest :target: https://secretstorage.readthedocs.io/en/latest/ :alt: ReadTheDocs status Module description ================== This module provides a way for securely storing passwords and other secrets. It uses D-Bus `Secret Service`_ API that is supported by GNOME Keyring, KWallet (since version 5.97) and KeePassXC. The main classes provided are ``secretstorage.Item``, representing a secret item (that has a *label*, a *secret* and some *attributes*) and ``secretstorage.Collection``, a place items are stored in. SecretStorage supports most of the functions provided by Secret Service, including creating and deleting items and collections, editing items, locking and unlocking collections (asynchronous unlocking is also supported). The documentation can be found on `secretstorage.readthedocs.io`_. .. _`Secret Service`: https://specifications.freedesktop.org/secret-service/ .. _`secretstorage.readthedocs.io`: https://secretstorage.readthedocs.io/en/latest/ Building the module =================== .. note:: SecretStorage 3.x supports Python 3.6 and newer versions. If you have an older version of Python, install SecretStorage 2.x:: pip install "SecretStorage < 3" SecretStorage requires these packages to work: * Jeepney_ * `python-cryptography`_ To build SecretStorage, use this command:: python3 setup.py build If you have Sphinx_ installed, you can also build the documentation:: python3 setup.py build_sphinx .. _Jeepney: https://pypi.org/project/jeepney/ .. _`python-cryptography`: https://pypi.org/project/cryptography/ .. _Sphinx: http://sphinx-doc.org/ Testing the module ================== First, make sure that you have the Secret Service daemon installed. The `GNOME Keyring`_ is the reference server-side implementation for the Secret Service specification. .. _`GNOME Keyring`: https://download.gnome.org/sources/gnome-keyring/ Then, start the daemon and unlock the ``default`` collection, if needed. The testsuite will fail to run if the ``default`` collection exists and is locked. If it does not exist, the testsuite can also use the temporary ``session`` collection, as provided by the GNOME Keyring. Then, run the Python unittest module:: python3 -m unittest discover -s tests If you want to run the tests in an isolated or headless environment, run this command in a D-Bus session:: dbus-run-session -- python3 -m unittest discover -s tests Get the code ============ SecretStorage is available under BSD license. The source code can be found on GitHub_. .. _GitHub: https://github.com/mitya57/secretstorage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407528.0 SecretStorage-3.3.3/SecretStorage.egg-info/SOURCES.txt0000644000175000017500000000142014275747350022077 0ustar00dmitrydmitryLICENSE MANIFEST.in README.rst changelog pyproject.toml setup.cfg setup.py SecretStorage.egg-info/PKG-INFO SecretStorage.egg-info/SOURCES.txt SecretStorage.egg-info/dependency_links.txt SecretStorage.egg-info/requires.txt SecretStorage.egg-info/top_level.txt docs/changelog.rst docs/collection.rst docs/conf.py docs/exceptions.rst docs/index.rst docs/item.rst docs/util.rst secretstorage/__init__.py secretstorage/collection.py secretstorage/defines.py secretstorage/dhcrypto.py secretstorage/exceptions.py secretstorage/item.py secretstorage/py.typed secretstorage/util.py tests/__init__.py tests/cleanup_test_items.py tests/run_tests.py tests/test_collection.py tests/test_context_manager.py tests/test_dhcrypto.py tests/test_exceptions.py tests/test_item.py tests/test_unlocking.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407528.0 SecretStorage-3.3.3/SecretStorage.egg-info/dependency_links.txt0000644000175000017500000000000114275747350024264 0ustar00dmitrydmitry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407528.0 SecretStorage-3.3.3/SecretStorage.egg-info/requires.txt0000644000175000017500000000003714275747350022616 0ustar00dmitrydmitrycryptography>=2.0 jeepney>=0.6 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407528.0 SecretStorage-3.3.3/SecretStorage.egg-info/top_level.txt0000644000175000017500000000001614275747350022745 0ustar00dmitrydmitrysecretstorage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407277.0 SecretStorage-3.3.3/changelog0000644000175000017500000001615514275746755015644 0ustar00dmitrydmitrySecretStorage 3.3.3, 2022-08-13 =============================== * Handle case when CreateItem method returns a prompt [`#39`_]. * Reformatted code in accordance with :PEP:`8` standard. .. _`#39`: https://github.com/mitya57/secretstorage/issues/39 SecretStorage 3.3.2, 2022-04-19 =============================== * Fixed a deprecation warning with jeepney 0.8. Thanks to Sam McKelvie for the pull request! SecretStorage 3.3.1, 2021-02-09 =============================== * Fixed a deprecation warning from cryptography module. Thanks to Jante Jomppanen for the pull request! * Added a :PEP:`561` ``py.typed`` file to declare typing support. SecretStorage 3.3.0, 2020-11-24 =============================== * Use new-style Jeepney blocking I/O API. Thanks Thomas Kluyver for the pull request! * Python ≥ 3.6 and Jeepney ≥ 0.6 are now required. SecretStorage 3.2.0, 2020-11-07 =============================== * Added helper function ``check_service_availability`` for checking if the Secret Service daemon is available without using it. SecretStorage 3.1.2, 2020-01-08 =============================== * Updated the docs to describe how to close the D-Bus connection after use. * For secrets of wrong type, a TypeError is now raised [`#20`_]. .. _`#20`: https://github.com/mitya57/secretstorage/issues/20 SecretStorage 3.1.1, 2019-01-24 =============================== * Fixes TypeError with cryptography 2.5. Thanks Zach Hoffman for the pull request! SecretStorage 3.1.0, 2018-09-02 =============================== * The ``dbus_init`` function no longer accepts any arguments. * The ``dbus_init`` function now converts ``ConnectionError`` and ``ValueError`` to ``SecretServiceNotAvailableException``. * New exception class: ``PromptDismissedException``. * Switched to declarative setup configuration. Build now requires setuptools 30.3 or newer. * Added support for prompts when deleting collections and items. * Added type annotations to all methods. SecretStorage 3.0.1, 2018-04-24 =============================== * When ``DBUS_SESSION_BUS_ADDRESS`` environment variable is unset, and Jeepney raises a ``KeyError`` because of that, SecretStorage now intercepts that error and re-raises it as ``SecretServiceNotAvailableException``. * Uploaded to PyPI with fixed meta-data. SecretStorage 3.0.0, 2018-04-23 =============================== .. warning:: This release is backwards incompatible with the previous versions. * Python 3.5 or newer is now required. * SecretStorage has been ported from dbus-python to Jeepney_, a pure Python D-Bus client. * The asynchronous API has been removed. If you need it, please file a bug and describe your use case. * The ``bus`` argument is now called ``connection`` in all functions that accept it. .. _Jeepney: https://pypi.org/project/jeepney/ SecretStorage 2.3.1, 2016-08-27 =============================== * Update requires line in setup.py for cryptography port. * Documentation is now hosted on ReadTheDocs_. .. _ReadTheDocs: https://secretstorage.readthedocs.io/en/latest/ SecretStorage 2.3.0, 2016-08-17 =============================== * Ported from PyCrypto to cryptography module [`#6`_]. * ``Item.get_secret()`` now returns a bytes object rather than a bytearray. .. _`#6`: https://github.com/mitya57/secretstorage/issues/6 SecretStorage 2.2.1, 2016-06-27 =============================== * Made dbus-python dependency optional because compiling it from sources is not an option for many users. See issues #4 and #5 for details. SecretStorage 2.2.0, 2016-06-18 =============================== * Deprecated compatibility functions are dropped. * Installation from PyPI now pulls in dbus-python. * Travis CI tests added. * Other minor fixes, simplifications and improvements. SecretStorage 2.1.4, 2016-01-10 =============================== * Catch AccessDenied errors in dbus_init() function. * Documentation improvements. SecretStorage 2.1.3, 2015-12-20 =============================== * Python 2.6 is no longer supported. * Compatibility functions are now deprecated and will be removed in the next major release. * Other minor fixes, simplifications and improvements. SecretStorage 2.1.2, 2015-06-30 =============================== * Add Item.unlock() method. * Use setuptools when it is available. * Documentation now uses Alabaster sphinx theme. * Other documentation fixes and improvements. SecretStorage 2.1.1, 2014-07-12 =============================== * Fixed a bug where common secret was incorrectly generated sometimes (one time of 128). * Other minor improvements. SecretStorage 2.1, 2014-05-28 ============================= * Support running tests with GNOME Keyring when there is no default collection. * When D-Bus main loop is already set, do not set it again. * Make dhcrypto module work with Python < 2.7.4. SecretStorage 2.0, 2014-01-27 ============================= * Add support for encrypted sessions and use them by default. * Get rid of Introspect() calls to make D-Bus messaging faster. SecretStorage 1.1, 2013-11-15 ============================= * Ported to PyQt5. * Added ``Item.get_created()`` method. * Improvements to error handling. SecretStorage 1.0, 2013-05-08 ============================= * Renamed ``exec_prompt_async_*`` functions to just ``exec_prompt_*`` (old aliases kept for compatibility purposes). * Added two helper functions: - ``get_any_collection()`` for getting default or session collection; - ``get_default_collection()`` for getting or creating the default collection. * Fix creation of items with empty attributes dict. * Make ``SecretServiceNotAvailableException`` a subclass of ``SecretStorageException``. * Various documentation improvements. SecretStorage 0.9, 2013-03-05 ============================= * Added support for content types in secrets. * Added ``Item.get_modified()`` method. * Added ``get_all_collections()`` and ``get_collection_by_alias()`` functions. * Added ``search_items()`` function for global search. * Made synchronous version of ``Collection.unlock()`` return a boolean representing whether the operation was dismissed. * Fixed wrong parsing of Secret Service result in ``Collection.get_modified()``. * Various test suite and documentation improvements. SecretStorage 0.8, 2013-01-05 ============================= * Added ``Collection`` and ``Item`` classes. * Added support for creating and deleting collections. * Added synchronous loop unlocking support. * Added support for PyQt applications. * Added test suite. * Convert D-Bus exceptions to SecretStorage exceptions. * Rewrote the documentation in Sphinx. * Miscellaneous internal improvements. SecretStorage 0.2, 2012-06-22 ============================= * Added ``get_item_attributes`` function. * Renamed ``get_item_ids`` to ``get_items_ids``. * Renamed ``get_item_by_id`` to ``get_item``. * Renamed ``delete_item_by_id`` to ``delete_item``. * Made ``create_item`` return id of the created item. * Added ``secretstorage.exceptions`` module. * Made all functions throw exceptions from that module. * Updated the documentation. * Added ``delete_test_items.py`` script that deletes all test items. SecretStorage 0.1, 2012-06-02 ============================= * Initial release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/docs/0000755000175000017500000000000014275747351014703 5ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/docs/changelog.rst0000644000175000017500000000047114224240652017350 0ustar00dmitrydmitry======================= SecretStorage changelog ======================= This changelog only lists the most important changes that happened in SecretStorage. Please see the `Git log`_ for the full list of changes. .. _`Git log`: https://github.com/mitya57/secretstorage/commits/master .. include:: ../changelog ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/docs/collection.rst0000644000175000017500000000025714224240652017556 0ustar00dmitrydmitry======================================= The ``secretstorage.collection`` module ======================================= .. automodule:: secretstorage.collection :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406945.0 SecretStorage-3.3.3/docs/conf.py0000644000175000017500000000454414275746241016206 0ustar00dmitrydmitry#!/usr/bin/env python3 import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'SecretStorage' copyright = '2022, Dmitry Shachnev' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '3.3' # The full version, including alpha/beta/rc tags. release = '3.3.3' # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { 'description': 'Python bindings to the FreeDesktop.org Secret Service API', 'description_font_style': 'italic', 'github_button': False, 'sidebar_width': '260px', } # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': ['about.html', 'navigation.html', 'sourcelink.html', 'searchbox.html'] } # -- Options for LaTeX output -------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'SecretStorage.tex', 'SecretStorage Documentation', 'Dmitry Shachnev', 'manual'), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/docs/exceptions.rst0000644000175000017500000000016314224240652017600 0ustar00dmitrydmitry=================== Possible exceptions =================== .. automodule:: secretstorage.exceptions :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658093206.0 SecretStorage-3.3.3/docs/index.rst0000644000175000017500000000605114265077226016542 0ustar00dmitrydmitry======================================= Welcome to SecretStorage documentation! ======================================= This module provides a way for securely storing passwords and other secrets. It uses `D-Bus`_-based FreeDesktop.org `Secret Service`_ standard that is, for example, supported by `GNOME Keyring`_ (since version 2.30), KWallet_ (since version 5.97) and KeePassXC_. It allows one to create new secret items, delete and search for passwords matching given attributes. It also supports graphical prompts when unlocking is needed. .. _`D-Bus`: https://www.freedesktop.org/wiki/Software/dbus .. _`Secret Service`: https://specifications.freedesktop.org/secret-service/ .. _`GNOME Keyring`: https://wiki.gnome.org/Projects/GnomeKeyring .. _KWallet: https://invent.kde.org/frameworks/kwallet .. _KeePassXC: https://avaldes.co/2020/01/28/secret-service-keepassxc.html SecretStorage code is hosted on GitHub_. .. _GitHub: https://github.com/mitya57/secretstorage Initializing D-Bus ================== .. seealso:: If you don't know how D-Bus works, please read `Introduction to D-Bus`_ firstly. .. _`Introduction to D-Bus`: https://www.freedesktop.org/wiki/IntroductionToDBus Before using SecretStorage, you need to initialize D-Bus. This can be done using this function: .. autofunction:: secretstorage.dbus_init If you need to quickly check whether the Secret Service daemon is available (either running or `activatable via D-Bus`_) without trying to call any its methods, you can use the following function: .. autofunction:: secretstorage.check_service_availability .. _`activatable via D-Bus`: https://www.freedesktop.org/wiki/IntroductionToDBus/#activation Examples of using SecretStorage =============================== Creating a new item in the default collection: >>> import secretstorage >>> connection = secretstorage.dbus_init() >>> collection = secretstorage.get_default_collection(connection) >>> attributes = {'application': 'myapp', 'another attribute': ... 'another value'} >>> item = collection.create_item('My first item', attributes, ... b'pa$$word') Getting item's label, attributes and secret: >>> item.get_label() 'My first item' >>> item.get_attributes() {'another attribute': 'another value', 'application': 'myapp'} >>> item.get_secret() b'pa$$word' Locking and unlocking collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The current version of SecretStorage provides only the synchronous API for locking and unlocking. This means that if prompting the user for a password is needed, then :meth:`~secretstorage.collection.Collection.unlock` call will block until the password is entered. >>> collection.lock() >>> collection.is_locked() True >>> collection.unlock() >>> collection.is_locked() False If you want to use the asynchronous API, please `file a bug`_ and describe your use case. .. _`file a bug`: https://github.com/mitya57/secretstorage/issues/new Contents ======== .. toctree:: :maxdepth: 2 collection item util exceptions changelog Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/docs/item.rst0000644000175000017500000000022714224240652016356 0ustar00dmitrydmitry================================= The ``secretstorage.item`` module ================================= .. automodule:: secretstorage.item :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/docs/util.rst0000644000175000017500000000030514224240652016372 0ustar00dmitrydmitry============================ Additional utility functions ============================ .. automodule:: secretstorage.util :members: format_secret, exec_prompt, unlock_objects ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/pyproject.toml0000644000175000017500000000007014224240652016646 0ustar00dmitrydmitry[build-system] requires = ["setuptools>=30.3", "wheel"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/secretstorage/0000755000175000017500000000000014275747351016625 5ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660407019.0 SecretStorage-3.3.3/secretstorage/__init__.py0000644000175000017500000000644414275746353020747 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2013-2020 # License: 3-clause BSD, see LICENSE file """This file provides quick access to all SecretStorage API. Please refer to documentation of individual modules for API details. """ from jeepney.bus_messages import message_bus from jeepney.io.blocking import DBusConnection, Proxy, open_dbus_connection from secretstorage.collection import Collection, create_collection, \ get_all_collections, get_default_collection, get_any_collection, \ get_collection_by_alias, search_items from secretstorage.item import Item from secretstorage.exceptions import SecretStorageException, \ SecretServiceNotAvailableException, LockedException, \ ItemNotFoundException, PromptDismissedException from secretstorage.util import add_match_rules __version_tuple__ = (3, 3, 3) __version__ = '.'.join(map(str, __version_tuple__)) __all__ = [ 'Collection', 'Item', 'ItemNotFoundException', 'LockedException', 'PromptDismissedException', 'SecretServiceNotAvailableException', 'SecretStorageException', 'check_service_availability', 'create_collection', 'dbus_init', 'get_all_collections', 'get_any_collection', 'get_collection_by_alias', 'get_default_collection', 'search_items', ] def dbus_init() -> DBusConnection: """Returns a new connection to the session bus, instance of jeepney's :class:`DBusConnection` class. This connection can then be passed to various SecretStorage functions, such as :func:`~secretstorage.collection.get_default_collection`. .. warning:: The D-Bus socket will not be closed automatically. You can close it manually using the :meth:`DBusConnection.close` method, or you can use the :class:`contextlib.closing` context manager: .. code-block:: python from contextlib import closing with closing(dbus_init()) as conn: collection = secretstorage.get_default_collection(conn) items = collection.search_items({'application': 'myapp'}) However, you will not be able to call any methods on the objects created within the context after you leave it. .. versionchanged:: 3.0 Before the port to Jeepney, this function returned an instance of :class:`dbus.SessionBus` class. .. versionchanged:: 3.1 This function no longer accepts any arguments. """ try: connection = open_dbus_connection() add_match_rules(connection) return connection except KeyError as ex: # os.environ['DBUS_SESSION_BUS_ADDRESS'] may raise it reason = "Environment variable {} is unset".format(ex.args[0]) raise SecretServiceNotAvailableException(reason) from ex except (ConnectionError, ValueError) as ex: raise SecretServiceNotAvailableException(str(ex)) from ex def check_service_availability(connection: DBusConnection) -> bool: """Returns True if the Secret Service daemon is either running or available for activation via D-Bus, False otherwise. .. versionadded:: 3.2 """ from secretstorage.util import BUS_NAME proxy = Proxy(message_bus, connection) return (proxy.NameHasOwner(BUS_NAME)[0] == 1 or BUS_NAME in proxy.ListActivatableNames()[0]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406974.0 SecretStorage-3.3.3/secretstorage/collection.py0000644000175000017500000002233414275746276021343 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2013-2022 # License: 3-clause BSD, see LICENSE file """Collection is a place where secret items are stored. Normally, only the default collection should be used, but this module allows to use any registered collection. Use :func:`get_default_collection` to get the default collection (and create it, if necessary). Collections are usually automatically unlocked when user logs in, but collections can also be locked and unlocked using :meth:`Collection.lock` and :meth:`Collection.unlock` methods (unlocking requires showing the unlocking prompt to user and can be synchronous or asynchronous). Creating new items and editing existing ones is possible only in unlocked collection.""" from typing import Dict, Iterator, Optional from jeepney.io.blocking import DBusConnection from secretstorage.defines import SS_PREFIX, SS_PATH from secretstorage.dhcrypto import Session from secretstorage.exceptions import LockedException, ItemNotFoundException, \ PromptDismissedException from secretstorage.item import Item from secretstorage.util import DBusAddressWrapper, exec_prompt, \ format_secret, open_session, unlock_objects COLLECTION_IFACE = SS_PREFIX + 'Collection' SERVICE_IFACE = SS_PREFIX + 'Service' DEFAULT_COLLECTION = '/org/freedesktop/secrets/aliases/default' SESSION_COLLECTION = '/org/freedesktop/secrets/collection/session' class Collection: """Represents a collection.""" def __init__(self, connection: DBusConnection, collection_path: str = DEFAULT_COLLECTION, session: Optional[Session] = None) -> None: self.connection = connection self.session = session self.collection_path = collection_path self._collection = DBusAddressWrapper( collection_path, COLLECTION_IFACE, connection) self._collection.get_property('Label') def is_locked(self) -> bool: """Returns :const:`True` if item is locked, otherwise :const:`False`.""" return bool(self._collection.get_property('Locked')) def ensure_not_locked(self) -> None: """If collection is locked, raises :exc:`~secretstorage.exceptions.LockedException`.""" if self.is_locked(): raise LockedException('Collection is locked!') def unlock(self) -> bool: """Requests unlocking the collection. Returns a boolean representing whether the prompt has been dismissed; that means :const:`False` on successful unlocking and :const:`True` if it has been dismissed. .. versionchanged:: 3.0 No longer accepts the ``callback`` argument. """ return unlock_objects(self.connection, [self.collection_path]) def lock(self) -> None: """Locks the collection.""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, self.connection) service.call('Lock', 'ao', [self.collection_path]) def delete(self) -> None: """Deletes the collection and all items inside it.""" self.ensure_not_locked() prompt, = self._collection.call('Delete', '') if prompt != "/": dismissed, _result = exec_prompt(self.connection, prompt) if dismissed: raise PromptDismissedException('Prompt dismissed.') def get_all_items(self) -> Iterator[Item]: """Returns a generator of all items in the collection.""" for item_path in self._collection.get_property('Items'): yield Item(self.connection, item_path, self.session) def search_items(self, attributes: Dict[str, str]) -> Iterator[Item]: """Returns a generator of items with the given attributes. `attributes` should be a dictionary.""" result, = self._collection.call('SearchItems', 'a{ss}', attributes) for item_path in result: yield Item(self.connection, item_path, self.session) def get_label(self) -> str: """Returns the collection label.""" label = self._collection.get_property('Label') assert isinstance(label, str) return label def set_label(self, label: str) -> None: """Sets collection label to `label`.""" self.ensure_not_locked() self._collection.set_property('Label', 's', label) def create_item(self, label: str, attributes: Dict[str, str], secret: bytes, replace: bool = False, content_type: str = 'text/plain') -> Item: """Creates a new :class:`~secretstorage.item.Item` with given `label` (unicode string), `attributes` (dictionary) and `secret` (bytestring). If `replace` is :const:`True`, replaces the existing item with the same attributes. If `content_type` is given, also sets the content type of the secret (``text/plain`` by default). Returns the created item.""" self.ensure_not_locked() if not self.session: self.session = open_session(self.connection) _secret = format_secret(self.session, secret, content_type) properties = { SS_PREFIX + 'Item.Label': ('s', label), SS_PREFIX + 'Item.Attributes': ('a{ss}', attributes), } item_path, prompt = self._collection.call( 'CreateItem', 'a{sv}(oayays)b', properties, _secret, replace ) if len(item_path) > 1: return Item(self.connection, item_path, self.session) dismissed, result = exec_prompt(self.connection, prompt) if dismissed: raise PromptDismissedException('Prompt dismissed.') signature, item_path = result assert signature == 'o' return Item(self.connection, item_path, self.session) def create_collection(connection: DBusConnection, label: str, alias: str = '', session: Optional[Session] = None) -> Collection: """Creates a new :class:`Collection` with the given `label` and `alias` and returns it. This action requires prompting. :raises: :exc:`~secretstorage.exceptions.PromptDismissedException` if the prompt is dismissed. """ if not session: session = open_session(connection) properties = {SS_PREFIX + 'Collection.Label': ('s', label)} service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) collection_path, prompt = service.call('CreateCollection', 'a{sv}s', properties, alias) if len(collection_path) > 1: return Collection(connection, collection_path, session=session) dismissed, result = exec_prompt(connection, prompt) if dismissed: raise PromptDismissedException('Prompt dismissed.') signature, collection_path = result assert signature == 'o' return Collection(connection, collection_path, session=session) def get_all_collections(connection: DBusConnection) -> Iterator[Collection]: """Returns a generator of all available collections.""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) for collection_path in service.get_property('Collections'): yield Collection(connection, collection_path) def get_default_collection(connection: DBusConnection, session: Optional[Session] = None) -> Collection: """Returns the default collection. If it doesn't exist, creates it.""" try: return Collection(connection) except ItemNotFoundException: return create_collection(connection, 'Default', 'default', session) def get_any_collection(connection: DBusConnection) -> Collection: """Returns any collection, in the following order of preference: - The default collection; - The "session" collection (usually temporary); - The first collection in the collections list.""" try: return Collection(connection) except ItemNotFoundException: pass try: # GNOME Keyring provides session collection where items # are stored in process memory. return Collection(connection, SESSION_COLLECTION) except ItemNotFoundException: pass collections = list(get_all_collections(connection)) if collections: return collections[0] else: raise ItemNotFoundException('No collections found.') def get_collection_by_alias(connection: DBusConnection, alias: str) -> Collection: """Returns the collection with the given `alias`. If there is no such collection, raises :exc:`~secretstorage.exceptions.ItemNotFoundException`.""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) collection_path, = service.call('ReadAlias', 's', alias) if len(collection_path) <= 1: raise ItemNotFoundException('No collection with such alias.') return Collection(connection, collection_path) def search_items(connection: DBusConnection, attributes: Dict[str, str]) -> Iterator[Item]: """Returns a generator of items in all collections with the given attributes. `attributes` should be a dictionary.""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) locked, unlocked = service.call('SearchItems', 'a{ss}', attributes) for item_path in locked + unlocked: yield Item(connection, item_path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660405806.0 SecretStorage-3.3.3/secretstorage/defines.py0000644000175000017500000000144714275744056020621 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2013-2016 # License: 3-clause BSD, see LICENSE file # This file contains some common defines. SS_PREFIX = 'org.freedesktop.Secret.' SS_PATH = '/org/freedesktop/secrets' DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' DBUS_ACCESS_DENIED = 'org.freedesktop.DBus.Error.AccessDenied' DBUS_SERVICE_UNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' DBUS_EXEC_FAILED = 'org.freedesktop.DBus.Error.Spawn.ExecFailed' DBUS_NO_REPLY = 'org.freedesktop.DBus.Error.NoReply' DBUS_NOT_SUPPORTED = 'org.freedesktop.DBus.Error.NotSupported' DBUS_NO_SUCH_OBJECT = 'org.freedesktop.Secret.Error.NoSuchObject' ALGORITHM_PLAIN = 'plain' ALGORITHM_DH = 'dh-ietf1024-sha256-aes128-cbc-pkcs7' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406663.0 SecretStorage-3.3.3/secretstorage/dhcrypto.py0000644000175000017500000000502214275745607021034 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2014-2018 # License: 3-clause BSD, see LICENSE file '''This module contains needed classes, functions and constants to implement dh-ietf1024-sha256-aes128-cbc-pkcs7 secret encryption algorithm.''' import hmac import math import os from hashlib import sha256 from typing import Optional # Needed for mypy # A standard 1024 bits (128 bytes) prime number for use in Diffie-Hellman exchange DH_PRIME_1024_BYTES = ( 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ) def int_to_bytes(number: int) -> bytes: return number.to_bytes(math.ceil(number.bit_length() / 8), 'big') DH_PRIME_1024 = int.from_bytes(DH_PRIME_1024_BYTES, 'big') class Session: def __init__(self) -> None: self.object_path = None # type: Optional[str] self.aes_key = None # type: Optional[bytes] self.encrypted = True # 128-bytes-long strong random number self.my_private_key = int.from_bytes(os.urandom(0x80), 'big') self.my_public_key = pow(2, self.my_private_key, DH_PRIME_1024) def set_server_public_key(self, server_public_key: int) -> None: common_secret_int = pow(server_public_key, self.my_private_key, DH_PRIME_1024) common_secret = int_to_bytes(common_secret_int) # Prepend NULL bytes if needed common_secret = b'\x00' * (0x80 - len(common_secret)) + common_secret # HKDF with null salt, empty info and SHA-256 hash salt = b'\x00' * 0x20 pseudo_random_key = hmac.new(salt, common_secret, sha256).digest() output_block = hmac.new(pseudo_random_key, b'\x01', sha256).digest() # Resulting AES key should be 128-bit self.aes_key = output_block[:0x10] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660405659.0 SecretStorage-3.3.3/secretstorage/exceptions.py0000644000175000017500000000316714275743633021366 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2012-2018 # License: 3-clause BSD, see LICENSE file """All secretstorage functions may raise various exceptions when something goes wrong. All exceptions derive from base :exc:`SecretStorageException` class.""" class SecretStorageException(Exception): """All exceptions derive from this class.""" class SecretServiceNotAvailableException(SecretStorageException): """Raised by :class:`~secretstorage.item.Item` or :class:`~secretstorage.collection.Collection` constructors, or by other functions in the :mod:`secretstorage.collection` module, when the Secret Service API is not available.""" class LockedException(SecretStorageException): """Raised when an action cannot be performed because the collection is locked. Use :meth:`~secretstorage.collection.Collection.is_locked` to check if the collection is locked, and :meth:`~secretstorage.collection.Collection.unlock` to unlock it. """ class ItemNotFoundException(SecretStorageException): """Raised when an item does not exist or has been deleted. Example of handling: >>> import secretstorage >>> connection = secretstorage.dbus_init() >>> item_path = '/not/existing/path' >>> try: ... item = secretstorage.Item(connection, item_path) ... except secretstorage.ItemNotFoundException: ... print('Item not found!') ... Item not found! """ class PromptDismissedException(ItemNotFoundException): """Raised when a prompt was dismissed by the user. .. versionadded:: 3.1 """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406663.0 SecretStorage-3.3.3/secretstorage/item.py0000644000175000017500000001326514275745607020146 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2013-2018 # License: 3-clause BSD, see LICENSE file """SecretStorage item contains a *secret*, some *attributes* and a *label* visible to user. Editing all these properties and reading the secret is possible only when the :doc:`collection ` storing the item is unlocked. The collection can be unlocked using collection's :meth:`~secretstorage.collection.Collection.unlock` method.""" from typing import Dict, Optional from jeepney.io.blocking import DBusConnection from secretstorage.defines import SS_PREFIX from secretstorage.dhcrypto import Session from secretstorage.exceptions import LockedException, PromptDismissedException from secretstorage.util import DBusAddressWrapper, \ exec_prompt, open_session, format_secret, unlock_objects from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend ITEM_IFACE = SS_PREFIX + 'Item' class Item: """Represents a secret item.""" def __init__(self, connection: DBusConnection, item_path: str, session: Optional[Session] = None) -> None: self.item_path = item_path self._item = DBusAddressWrapper(item_path, ITEM_IFACE, connection) self._item.get_property('Label') self.session = session self.connection = connection def __eq__(self, other: "DBusConnection") -> bool: assert isinstance(other.item_path, str) return self.item_path == other.item_path def is_locked(self) -> bool: """Returns :const:`True` if item is locked, otherwise :const:`False`.""" return bool(self._item.get_property('Locked')) def ensure_not_locked(self) -> None: """If collection is locked, raises :exc:`~secretstorage.exceptions.LockedException`.""" if self.is_locked(): raise LockedException('Item is locked!') def unlock(self) -> bool: """Requests unlocking the item. Usually, this means that the whole collection containing this item will be unlocked. Returns a boolean representing whether the prompt has been dismissed; that means :const:`False` on successful unlocking and :const:`True` if it has been dismissed. .. versionadded:: 2.1.2 .. versionchanged:: 3.0 No longer accepts the ``callback`` argument. """ return unlock_objects(self.connection, [self.item_path]) def get_attributes(self) -> Dict[str, str]: """Returns item attributes (dictionary).""" attrs = self._item.get_property('Attributes') return dict(attrs) def set_attributes(self, attributes: Dict[str, str]) -> None: """Sets item attributes to `attributes` (dictionary).""" self._item.set_property('Attributes', 'a{ss}', attributes) def get_label(self) -> str: """Returns item label (unicode string).""" label = self._item.get_property('Label') assert isinstance(label, str) return label def set_label(self, label: str) -> None: """Sets item label to `label`.""" self.ensure_not_locked() self._item.set_property('Label', 's', label) def delete(self) -> None: """Deletes the item.""" self.ensure_not_locked() prompt, = self._item.call('Delete', '') if prompt != "/": dismissed, _result = exec_prompt(self.connection, prompt) if dismissed: raise PromptDismissedException('Prompt dismissed.') def get_secret(self) -> bytes: """Returns item secret (bytestring).""" self.ensure_not_locked() if not self.session: self.session = open_session(self.connection) secret, = self._item.call('GetSecret', 'o', self.session.object_path) if not self.session.encrypted: return bytes(secret[2]) assert self.session.aes_key is not None aes = algorithms.AES(self.session.aes_key) aes_iv = bytes(secret[1]) decryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).decryptor() encrypted_secret = secret[2] padded_secret = decryptor.update(bytes(encrypted_secret)) + decryptor.finalize() assert isinstance(padded_secret, bytes) return padded_secret[:-padded_secret[-1]] def get_secret_content_type(self) -> str: """Returns content type of item secret (string).""" self.ensure_not_locked() if not self.session: self.session = open_session(self.connection) secret, = self._item.call('GetSecret', 'o', self.session.object_path) return str(secret[3]) def set_secret(self, secret: bytes, content_type: str = 'text/plain') -> None: """Sets item secret to `secret`. If `content_type` is given, also sets the content type of the secret (``text/plain`` by default).""" self.ensure_not_locked() if not self.session: self.session = open_session(self.connection) _secret = format_secret(self.session, secret, content_type) self._item.call('SetSecret', '(oayays)', _secret) def get_created(self) -> int: """Returns UNIX timestamp (integer) representing the time when the item was created. .. versionadded:: 1.1""" created = self._item.get_property('Created') assert isinstance(created, int) return created def get_modified(self) -> int: """Returns UNIX timestamp (integer) representing the time when the item was last modified.""" modified = self._item.get_property('Modified') assert isinstance(modified, int) return modified ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649492394.0 SecretStorage-3.3.3/secretstorage/py.typed0000644000175000017500000000000014224240652020274 0ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660405527.0 SecretStorage-3.3.3/secretstorage/util.py0000644000175000017500000001514314275743427020160 0ustar00dmitrydmitry# SecretStorage module for Python # Access passwords using the SecretService DBus API # Author: Dmitry Shachnev, 2013-2018 # License: 3-clause BSD, see LICENSE file """This module provides some utility functions, but these shouldn't normally be used by external applications.""" import os from typing import Any, List, Tuple from jeepney import ( DBusAddress, DBusErrorResponse, MatchRule, Message, MessageType, new_method_call, Properties, ) from jeepney.io.blocking import DBusConnection from secretstorage.defines import DBUS_UNKNOWN_METHOD, DBUS_NO_SUCH_OBJECT, \ DBUS_SERVICE_UNKNOWN, DBUS_NO_REPLY, DBUS_NOT_SUPPORTED, DBUS_EXEC_FAILED, \ SS_PATH, SS_PREFIX, ALGORITHM_DH, ALGORITHM_PLAIN from secretstorage.dhcrypto import Session, int_to_bytes from secretstorage.exceptions import ItemNotFoundException, \ SecretServiceNotAvailableException from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend BUS_NAME = 'org.freedesktop.secrets' SERVICE_IFACE = SS_PREFIX + 'Service' PROMPT_IFACE = SS_PREFIX + 'Prompt' class DBusAddressWrapper(DBusAddress): # type: ignore """A wrapper class around :class:`jeepney.wrappers.DBusAddress` that adds some additional methods for calling and working with properties, and converts error responses to SecretStorage exceptions. .. versionadded:: 3.0 """ def __init__(self, path: str, interface: str, connection: DBusConnection) -> None: DBusAddress.__init__(self, path, BUS_NAME, interface) self._connection = connection def send_and_get_reply(self, msg: Message) -> Any: try: resp_msg: Message = self._connection.send_and_get_reply(msg) if resp_msg.header.message_type == MessageType.error: raise DBusErrorResponse(resp_msg) return resp_msg.body except DBusErrorResponse as resp: if resp.name in (DBUS_UNKNOWN_METHOD, DBUS_NO_SUCH_OBJECT): raise ItemNotFoundException('Item does not exist!') from resp elif resp.name in (DBUS_SERVICE_UNKNOWN, DBUS_EXEC_FAILED, DBUS_NO_REPLY): data = resp.data if isinstance(data, tuple): data = data[0] raise SecretServiceNotAvailableException(data) from resp raise def call(self, method: str, signature: str, *body: Any) -> Any: msg = new_method_call(self, method, signature, body) return self.send_and_get_reply(msg) def get_property(self, name: str) -> Any: msg = Properties(self).get(name) (signature, value), = self.send_and_get_reply(msg) return value def set_property(self, name: str, signature: str, value: Any) -> None: msg = Properties(self).set(name, signature, value) self.send_and_get_reply(msg) def open_session(connection: DBusConnection) -> Session: """Returns a new Secret Service session.""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) session = Session() try: output, result = service.call( 'OpenSession', 'sv', ALGORITHM_DH, ('ay', int_to_bytes(session.my_public_key))) except DBusErrorResponse as resp: if resp.name != DBUS_NOT_SUPPORTED: raise output, result = service.call( 'OpenSession', 'sv', ALGORITHM_PLAIN, ('s', '')) session.encrypted = False else: signature, value = output assert signature == 'ay' key = int.from_bytes(value, 'big') session.set_server_public_key(key) session.object_path = result return session def format_secret(session: Session, secret: bytes, content_type: str) -> Tuple[str, bytes, bytes, str]: """Formats `secret` to make possible to pass it to the Secret Service API.""" if isinstance(secret, str): secret = secret.encode('utf-8') elif not isinstance(secret, bytes): raise TypeError('secret must be bytes') assert session.object_path is not None if not session.encrypted: return (session.object_path, b'', secret, content_type) assert session.aes_key is not None # PKCS-7 style padding padding = 0x10 - (len(secret) & 0xf) secret += bytes((padding,) * padding) aes_iv = os.urandom(0x10) aes = algorithms.AES(session.aes_key) encryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).encryptor() encrypted_secret = encryptor.update(secret) + encryptor.finalize() return ( session.object_path, aes_iv, encrypted_secret, content_type ) def exec_prompt(connection: DBusConnection, prompt_path: str) -> Tuple[bool, List[str]]: """Executes the prompt in a blocking mode. :returns: a tuple; the first element is a boolean value showing whether the operation was dismissed, the second element is a list of unlocked object paths """ prompt = DBusAddressWrapper(prompt_path, PROMPT_IFACE, connection) rule = MatchRule( path=prompt_path, interface=PROMPT_IFACE, member='Completed', type=MessageType.signal, ) with connection.filter(rule) as signals: prompt.call('Prompt', 's', '') dismissed, result = connection.recv_until_filtered(signals).body assert dismissed is not None assert result is not None return dismissed, result def unlock_objects(connection: DBusConnection, paths: List[str]) -> bool: """Requests unlocking objects specified in `paths`. Returns a boolean representing whether the operation was dismissed. .. versionadded:: 2.1.2""" service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection) unlocked_paths, prompt = service.call('Unlock', 'ao', paths) if len(prompt) > 1: dismissed, (signature, unlocked) = exec_prompt(connection, prompt) assert signature == 'ao' return dismissed return False def add_match_rules(connection: DBusConnection) -> None: """Adds match rules for the given connection. Currently it matches all messages from the Prompt interface, as the mock service (unlike GNOME Keyring) does not specify the signal destination. .. versionadded:: 3.1 """ rule = MatchRule(sender=BUS_NAME, interface=PROMPT_IFACE) dbus = DBusAddressWrapper(path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', connection=connection) dbus.bus_name = 'org.freedesktop.DBus' dbus.call('AddMatch', 's', rule.serialise()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/setup.cfg0000644000175000017500000000200514275747351015571 0ustar00dmitrydmitry[metadata] name = SecretStorage version = 3.3.3 description = Python bindings to FreeDesktop.org Secret Service API long_description = file: README.rst long_description_content_type = text/x-rst author = Dmitry Shachnev author_email = mitya57@gmail.com url = https://github.com/mitya57/secretstorage license = BSD 3-Clause License classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Operating System :: POSIX Programming Language :: Python Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Security Topic :: Software Development :: Libraries :: Python Modules platforms = Linux [options] packages = secretstorage python_requires = >=3.6 install_requires = cryptography>=2.0; jeepney>=0.6 [options.package_data] secretstorage = py.typed [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1652614807.0 SecretStorage-3.3.3/setup.py0000755000175000017500000000007514240163227015454 0ustar00dmitrydmitry#!/usr/bin/env python from setuptools import setup setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1660407528.7967653 SecretStorage-3.3.3/tests/0000755000175000017500000000000014275747351015115 5ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1652614807.0 SecretStorage-3.3.3/tests/__init__.py0000644000175000017500000000000014240163227017176 0ustar00dmitrydmitry././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406130.0 SecretStorage-3.3.3/tests/cleanup_test_items.py0000755000175000017500000000053314275744562021363 0ustar00dmitrydmitry#!/usr/bin/env python3 from contextlib import closing import secretstorage with closing(secretstorage.dbus_init()) as connection: items = secretstorage.search_items( connection, {'application': 'secretstorage-test'} ) for item in items: print('Deleting item with label %r.' % item.get_label()) item.delete() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406269.0 SecretStorage-3.3.3/tests/run_tests.py0000644000175000017500000000213114275744775017521 0ustar00dmitrydmitry#!/usr/bin/env python import os.path import sys import subprocess import unittest tests_dir = os.path.dirname(__file__) sys.path.insert(0, os.path.dirname(tests_dir)) import secretstorage # noqa if __name__ == '__main__': major, minor, patch = sys.version_info[:3] print('Running with Python %d.%d.%d (SecretStorage from %s)' % (major, minor, patch, os.path.dirname(secretstorage.__file__))) mock = None if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): mock = subprocess.Popen(('/usr/bin/python3', sys.argv[1],), stdout=subprocess.PIPE, universal_newlines=True) assert mock.stdout is not None # for mypy bus_name = mock.stdout.readline().rstrip() secretstorage.util.BUS_NAME = bus_name print('Bus name set to %r' % secretstorage.util.BUS_NAME) loader = unittest.TestLoader() runner = unittest.TextTestRunner(verbosity=2) result = runner.run(loader.discover(tests_dir)) if mock is not None: mock.terminate() sys.exit(not result.wasSuccessful()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406068.0 SecretStorage-3.3.3/tests/test_collection.py0000644000175000017500000000610414275744464020664 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2013-2018 # License: 3-clause BSD, see LICENSE file # This file tests the secretstorage.Collection class. import unittest from secretstorage import Collection, Item from secretstorage import dbus_init, get_any_collection, get_all_collections from secretstorage import create_collection, get_default_collection from secretstorage.util import BUS_NAME from secretstorage.exceptions import ItemNotFoundException class CollectionTest(unittest.TestCase): """A test case that tests that all common methods of Collection class work and do not crash.""" def setUp(self) -> None: self.connection = dbus_init() self.collection = get_any_collection(self.connection) def tearDown(self) -> None: self.connection.close() def test_all_collections(self) -> None: labels = map(Collection.get_label, get_all_collections(self.connection)) self.assertIn(self.collection.get_label(), labels) def test_all_items(self) -> None: for item in self.collection.get_all_items(): item.get_label() def test_create_empty_item(self) -> None: item = self.collection.create_item('', {}, b'') item.delete() def test_label(self) -> None: old_label = self.collection.get_label() self.collection.set_label('Hello!') self.assertEqual(self.collection.get_label(), 'Hello!') self.collection.set_label(old_label) self.assertEqual(self.collection.get_label(), old_label) @unittest.skipIf(BUS_NAME == "org.freedesktop.secrets", "This test should only be run with the mocked server.") class MockCollectionTest(unittest.TestCase): def setUp(self) -> None: self.connection = dbus_init() def tearDown(self) -> None: self.connection.close() def test_default_collection(self) -> None: collection = get_default_collection(self.connection) self.assertEqual(collection.get_label(), "Collection One") def test_deleting(self) -> None: collection_path = "/org/freedesktop/secrets/collection/spanish" collection = Collection(self.connection, collection_path) collection.unlock() collection.delete() def test_deleting_prompt(self) -> None: collection_path = "/org/freedesktop/secrets/collection/lockprompt" try: collection = Collection(self.connection, collection_path) except ItemNotFoundException: self.skipTest("This test should only be run with mock-service-lock.") collection.unlock() collection.delete() def test_deleting_item_prompt(self) -> None: item_path = "/org/freedesktop/secrets/collection/lockone/confirm" try: item = Item(self.connection, item_path) except ItemNotFoundException: self.skipTest("This test should only be run with mock-service-lock.") item.delete() def test_create_collection(self) -> None: collection = create_collection(self.connection, "My Label") self.assertEqual(collection.get_label(), "My Label") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1652614807.0 SecretStorage-3.3.3/tests/test_context_manager.py0000644000175000017500000000155114240163227021670 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2019 # License: 3-clause BSD, see LICENSE file # This file tests using secretstorage.dbus_init() function # together with contextlib.closing context manager. from contextlib import closing import unittest from secretstorage import check_service_availability, dbus_init from secretstorage.collection import get_any_collection class ContextManagerTest(unittest.TestCase): """``dbus_init()`` should work fine with ``contextlib.closing`` context manager.""" def test_closing_context_manager(self) -> None: with closing(dbus_init()) as connection: self.assertTrue(check_service_availability(connection)) collection = get_any_collection(connection) self.assertIsNotNone(collection) label = collection.get_label() self.assertIsNotNone(label) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406051.0 SecretStorage-3.3.3/tests/test_dhcrypto.py0000644000175000017500000000105414275744443020361 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2014-2016 # License: 3-clause BSD, see LICENSE file # This file tests the dhcrypto module. import unittest from secretstorage.dhcrypto import int_to_bytes class ConversionTest(unittest.TestCase): """A test case that tests conversion functions between bytes and long.""" def test_int_to_bytes(self) -> None: self.assertEqual(int_to_bytes(1), b'\x01') self.assertEqual(int_to_bytes(258), b'\x01\x02') self.assertEqual(int_to_bytes(1 << 64), b'\x01' + b'\x00' * 8) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406037.0 SecretStorage-3.3.3/tests/test_exceptions.py0000644000175000017500000000233614275744425020712 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2013-2018 # License: 3-clause BSD, see LICENSE file # Various exception tests import unittest import secretstorage from secretstorage.exceptions import ItemNotFoundException class ExceptionsTest(unittest.TestCase): """A test case that ensures that all SecretStorage exceptions are raised correctly.""" def setUp(self) -> None: self.connection = secretstorage.dbus_init() self.collection = secretstorage.get_any_collection(self.connection) def tearDown(self) -> None: self.connection.close() def test_double_deleting(self) -> None: item = self.collection.create_item( 'MyItem', {'application': 'secretstorage-test'}, b'pa$$word') item.delete() self.assertRaises(ItemNotFoundException, item.delete) def test_non_existing_item(self) -> None: self.assertRaises( ItemNotFoundException, secretstorage.Item, self.connection, '/not/existing/path') def test_non_existing_collection(self) -> None: self.assertRaises( ItemNotFoundException, secretstorage.get_collection_by_alias, self.connection, 'non-existing-alias') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1660406716.0 SecretStorage-3.3.3/tests/test_item.py0000644000175000017500000000726414275745674017503 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2013-2018 # License: 3-clause BSD, see LICENSE file # This file tests the secretstorage.Collection class. import unittest import time from secretstorage import dbus_init, search_items, get_any_collection ATTRIBUTES = {'application': 'secretstorage-test', 'attribute': 'qwerty'} NEW_ATTRIBUTES = {'application': 'secretstorage-test', 'newattribute': 'asdfgh'} class ItemTest(unittest.TestCase): """A test case that tests that all common methods of Item class work and do not crash.""" def setUp(self) -> None: self.connection = dbus_init() self.collection = get_any_collection(self.connection) self.created_timestamp = time.time() self.item = self.collection.create_item( 'My item', ATTRIBUTES, b'pa$$word') self.other_item = self.collection.create_item( 'My item', ATTRIBUTES, b'', content_type='data/null') def tearDown(self) -> None: self.item.delete() self.other_item.delete() self.connection.close() def test_equal(self) -> None: self.assertEqual(self.item, self.item) self.assertNotEqual(self.item, self.other_item) self.assertEqual(self.other_item, self.other_item) def test_searchable(self) -> None: search_results = self.collection.search_items(ATTRIBUTES) self.assertIn(self.item, search_results) search_results = search_items(self.connection, ATTRIBUTES) self.assertIn(self.item, search_results) def test_item_in_all_items(self) -> None: all_items = self.collection.get_all_items() self.assertIn(self.item, all_items) def test_attributes(self) -> None: attributes = self.item.get_attributes() for key in ATTRIBUTES: self.assertEqual(ATTRIBUTES[key], attributes[key]) self.item.set_attributes(NEW_ATTRIBUTES) attributes = self.item.get_attributes() for key in NEW_ATTRIBUTES: self.assertEqual(NEW_ATTRIBUTES[key], attributes[key]) self.item.set_attributes(ATTRIBUTES) def test_label(self) -> None: self.assertEqual(self.item.get_label(), 'My item') self.item.set_label('Hello!') self.assertEqual(self.item.get_label(), 'Hello!') def test_secret(self) -> None: self.assertEqual(self.item.get_secret(), b'pa$$word') self.item.set_secret(b'newpa$$word') self.assertIsInstance(self.item.get_secret(), bytes) self.assertEqual(self.item.get_secret(), b'newpa$$word') self.assertEqual(self.other_item.get_secret(), b'') def test_secret_wrong_type(self) -> None: # string passwords are encoded as bytes self.item.set_secret('test тест') # type: ignore self.assertEqual(self.item.get_secret(), 'test тест'.encode('utf-8')) # other types are not allowed with self.assertRaises(TypeError): self.item.set_secret(None) # type: ignore def test_secret_content_type(self) -> None: self.assertEqual(self.item.get_secret_content_type(), 'text/plain') # The check below fails in gnome-keyring because it doesn't really # support content types. # self.assertEqual(self.other_item.get_secret_content_type(), 'data/null') def test_modified(self) -> None: now = time.time() modified = self.item.get_modified() self.assertAlmostEqual(now, modified, places=-1) def test_created(self) -> None: created = self.item.get_created() self.assertAlmostEqual(self.created_timestamp, created, places=-1) def test_unlock(self) -> None: self.item.unlock() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1652614807.0 SecretStorage-3.3.3/tests/test_unlocking.py0000644000175000017500000000234314240163227020503 0ustar00dmitrydmitry# Tests for SecretStorage # Author: Dmitry Shachnev, 2018 # License: 3-clause BSD, see LICENSE file import unittest from secretstorage import dbus_init, Collection from secretstorage.util import BUS_NAME from secretstorage.exceptions import LockedException @unittest.skipIf(BUS_NAME == "org.freedesktop.secrets", "This test should only be run with the mocked server.") class LockingUnlockingTest(unittest.TestCase): def setUp(self) -> None: self.connection = dbus_init() collection_path = "/org/freedesktop/secrets/collection/english" self.collection = Collection(self.connection, collection_path) def tearDown(self) -> None: self.connection.close() def test_lock_unlock(self) -> None: self.assertFalse(self.collection.is_locked()) self.collection.lock() self.assertTrue(self.collection.is_locked()) self.assertRaises(LockedException, self.collection.ensure_not_locked) item, = self.collection.search_items({"number": "1"}) self.assertRaises(LockedException, item.ensure_not_locked) self.assertIs(self.collection.unlock(), False) self.assertFalse(self.collection.is_locked()) self.collection.ensure_not_locked()