././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6738575 keyrings.alt-5.0.0/0000755000175100001730000000000014457341023013532 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.coveragerc0000644000175100001730000000020514457340770015660 0ustar00runnerdocker[run] omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* disable_warnings = couldnt-parse [report] show_missing = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.editorconfig0000644000175100001730000000036614457340770016224 0ustar00runnerdockerroot = true [*] charset = utf-8 indent_style = tab indent_size = 4 insert_final_newline = true end_of_line = lf [*.py] indent_style = space max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.rst] indent_style = space ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/.github/0000755000175100001730000000000014457341023015072 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.github/FUNDING.yml0000644000175100001730000000003414457340770016714 0ustar00runnerdockertidelift: pypi/keyrings.alt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.github/dependabot.yml0000644000175100001730000000022414457340770017730 0ustar00runnerdockerversion: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" allow: - dependency-type: "all" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/.github/workflows/0000755000175100001730000000000014457341023017127 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.github/workflows/main.yml0000644000175100001730000000611214457340770020606 0ustar00runnerdockername: tests on: [push, pull_request] permissions: contents: read env: # Environment variables to support color support (jaraco/skeleton#66): # Request colored output from CLI tools supporting it. Different tools # interpret the value differently. For some, just being set is sufficient. # For others, it must be a non-zero integer. For yet others, being set # to a non-empty value is sufficient. For tox, it must be one of # , 0, 1, false, no, off, on, true, yes. The only enabling value # in common is "1". FORCE_COLOR: 1 # MyPy's color enforcement (must be a non-zero number) MYPY_FORCE_COLOR: -42 # Recognized by the `py` package, dependency of `pytest` (must be "1") PY_COLORS: 1 # Make tox-wrapped tools see color requests TOX_TESTENV_PASSENV: >- FORCE_COLOR MYPY_FORCE_COLOR NO_COLOR PY_COLORS PYTEST_THEME PYTEST_THEME_MODE # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream # Must be "1". TOX_PARALLEL_NO_SPINNER: 1 jobs: test: strategy: matrix: python: - "3.8" - "3.11" - "3.12" platform: - ubuntu-latest - macos-latest - windows-latest include: - python: "3.9" platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.12' }} steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox run: | python -m pip install tox - name: Run run: tox docs: runs-on: ubuntu-latest env: TOXENV: docs steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 - name: Install tox run: | python -m pip install tox - name: Run run: tox check: # This job does nothing and is only used for the branch protection if: always() needs: - test - docs runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} release: permissions: contents: write needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install tox run: | python -m pip install tox - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.pre-commit-config.yaml0000644000175100001730000000012114457340770020015 0ustar00runnerdockerrepos: - repo: https://github.com/psf/black rev: 22.6.0 hooks: - id: black ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/.readthedocs.yaml0000644000175100001730000000027414457340770016774 0ustar00runnerdockerversion: 2 python: install: - path: . extra_requirements: - docs # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-22.04 tools: python: "3" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/LICENSE0000644000175100001730000000177714457340770014563 0ustar00runnerdockerPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/NEWS.rst0000644000175100001730000000531014457340770015047 0ustar00runnerdockerv5.0.0 ====== Features -------- - Require Python 3.8 or later. Deprecations and Removals ------------------------- - Removed the pyfs backend, as it has eroded beyond repair. (#49) v4.2.0 ====== #46: EncryptedFileKeyring now supports both pycryptodome and pycryptodomex (preferring the latter). v4.1.2 ====== Updated to work with keyring 23.9+ (no longer depending on properties module). v4.1.1 ====== Refresh package metadata. Enrolled with Tidelift. v4.1.0 ====== #44: Bump upper bound on pyfs. Refresh package metadata. v4.0.2 ====== #43: Tests are no longer included in the install. v4.0.1 ====== Package refresh and minor cleanup. v4.0.0 ====== #41: Instead of PyCrypto or PyCryptodome, the encrypting backend now relies on PyCryptodomex. v3.5.2 ====== #39: Replace use of deprecated ``base64.encode/decodestring`` with ``encode/decodebytes``. v3.5.1 ====== #38: Fixed test suite to work with pytest-based fixtures. Refresh package metadata. v3.5.0 ====== #33: Rely on keyring.testing (keyring 20) for tests. v3.4.0 ====== In tests, pin keyring major version. v3.3.0 ====== Drop support for Python 3.5 and earlier. v3.2.0 ====== In tests, rely on pycryptodome instead of pycrypto for improved compatibility. In tests, rely on pytest instead of unittest. 3.1.1 ===== #31: Trap AttributeError in Gnome backend as in some environments it seems that will happen. #30: Fix issue where a backslash in the service name would cause errors on Registry backend on Windows. 3.1 === ``keyrings.alt`` no longer depends on the ``keyring.util.escape`` module. 3.0 === ``keyrings`` namespace should now use the pkgutil native technique rather than relying on pkg_resources. 2.4 === #24: File based backends now reject non-string types for passwords. 2.3 === #21: Raise ValueError on blank username in plaintext keyring, unsupported in the storage format. 2.2 === #17: Drop dependency on keyring.py27compat and use six instead. #16: Minor tweaks to file-based backends. 2.1 === Add persistent scheme and version tags for file based backends. Prepare for associated data handling in file based schemes. 2.0 === #12: Drop kwallet support, now superseded by the dual kwallet support in keyring. 1.3 === #9: Moved base file backend functionality from 'keyrings.alt.file' to 'keyrings.alt.base_file'. This allows the 'Windows' module to no longer trigger a circular import with the 'file' module. 1.2 === Updated project skeleton. Tests now run under tox. Tagged commits are automatically released to PyPI. #6: Added license file. 1.1.1 ===== Test cleanup. Exclude tests during install. 1.1 === FileBacked backends now have a ``repr`` that includes the file path. 1.0 === Initial release based on Keyring 7.3. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6738575 keyrings.alt-5.0.0/PKG-INFO0000644000175100001730000000516314457341023014634 0ustar00runnerdockerMetadata-Version: 2.1 Name: keyrings.alt Version: 5.0.0 Summary: Alternate keyring implementations Home-page: https://github.com/jaraco/keyrings.alt Author: Jason R. Coombs Author-email: jaraco@jaraco.com Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Requires-Python: >=3.8 Provides-Extra: testing Provides-Extra: docs License-File: LICENSE .. image:: https://img.shields.io/pypi/v/keyrings.alt.svg :target: https://pypi.org/project/keyrings.alt .. image:: https://img.shields.io/pypi/pyversions/keyrings.alt.svg .. image:: https://github.com/jaraco/keyrings.alt/workflows/tests/badge.svg :target: https://github.com/jaraco/keyrings.alt/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/keyrings.alt :target: https://tidelift.com/subscription/pkg/pypi-keyrings.alt?utm_source=pypi-keyrings.alt&utm_medium=readme Alternate keyring backend implementations for use with the `keyring package `_. Keyrings in this package may have security risks or other implications. These backends were extracted from the main keyring project to make them available for those who wish to employ them, but are discouraged for general production use. Include this module and use its backends at your own risk. For example, the PlaintextKeyring stores passwords in plain text on the file system, defeating the intended purpose of this library to encourage best practices for security. For Enterprise ============== Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/README.rst0000644000175100001730000000411414457340770015231 0ustar00runnerdocker.. image:: https://img.shields.io/pypi/v/keyrings.alt.svg :target: https://pypi.org/project/keyrings.alt .. image:: https://img.shields.io/pypi/pyversions/keyrings.alt.svg .. image:: https://github.com/jaraco/keyrings.alt/workflows/tests/badge.svg :target: https://github.com/jaraco/keyrings.alt/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/keyrings.alt :target: https://tidelift.com/subscription/pkg/pypi-keyrings.alt?utm_source=pypi-keyrings.alt&utm_medium=readme Alternate keyring backend implementations for use with the `keyring package `_. Keyrings in this package may have security risks or other implications. These backends were extracted from the main keyring project to make them available for those who wish to employ them, but are discouraged for general production use. Include this module and use its backends at your own risk. For example, the PlaintextKeyring stores passwords in plain text on the file system, defeating the intended purpose of this library to encourage best practices for security. For Enterprise ============== Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/SECURITY.md0000644000175100001730000000026414457340770015335 0ustar00runnerdocker# Security Contact To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/conftest.py0000644000175100001730000000020214457340770015733 0ustar00runnerdockerimport platform collect_ignore = [] if platform.system() != 'Windows': collect_ignore.append('keyrings/alt/_win_crypto.py') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/docs/0000755000175100001730000000000014457341023014462 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/docs/conf.py0000644000175100001730000000234514457340770015775 0ustar00runnerdockerextensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', ] master_doc = "index" html_theme = "furo" # Link dates and other references in the changelog extensions += ['rst.linker'] link_files = { '../NEWS.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( pattern=r'PEP[- ](?P\d+)', url='https://peps.python.org/pep-{pep_number:0>4}/', ), ], ) } # Be strict about any broken references nitpicky = True # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } # Preserve authored syntax for defaults autodoc_preserve_defaults = True extensions += ['jaraco.tidelift'] intersphinx_mapping.update( keyring=('https://keyring.readthedocs.io/en/latest/', None), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/docs/history.rst0000644000175100001730000000011614457340770016723 0ustar00runnerdocker:tocdepth: 2 .. _changes: History ******* .. include:: ../NEWS (links).rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/docs/index.rst0000644000175100001730000000165214457340770016337 0ustar00runnerdockerWelcome to |project| documentation! =================================== .. sidebar-links:: :home: :pypi: .. toctree:: :maxdepth: 1 history .. tidelift-referral-banner:: .. automodule:: keyrings.alt.file :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.Gnome :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.Google :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.keyczar :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.multi :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.Windows :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.file_base :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/keyrings/0000755000175100001730000000000014457341023015365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/__init__.py0000644000175100001730000000012114457340770017500 0ustar00runnerdocker__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/keyrings/alt/0000755000175100001730000000000014457341023016145 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/Gnome.py0000644000175100001730000001042514457340770017576 0ustar00runnerdockertry: import gi gi.require_version('GnomeKeyring', '1.0') from gi.repository import GnomeKeyring except (ImportError, ValueError, AttributeError): pass from jaraco.classes import properties from keyring.backend import KeyringBackend from keyring.errors import PasswordSetError, PasswordDeleteError class Keyring(KeyringBackend): """Gnome Keyring""" KEYRING_NAME = None """ Name of the keyring in which to store the passwords. Use None for the default keyring. """ @properties.classproperty def priority(cls): if 'GnomeKeyring' not in globals(): raise RuntimeError("GnomeKeyring module required") result = GnomeKeyring.get_default_keyring_sync()[0] if result != GnomeKeyring.Result.OK: raise RuntimeError(result.value_name) return 1 @property def keyring_name(self): system_default = GnomeKeyring.get_default_keyring_sync()[1] return self.KEYRING_NAME or system_default def _find_passwords(self, service, username, deleting=False): """Get password of the username for the service""" passwords = [] service = self._safe_string(service) username = self._safe_string(username) for attrs_tuple in (('username', 'service'), ('user', 'domain')): attrs = GnomeKeyring.Attribute.list_new() GnomeKeyring.Attribute.list_append_string(attrs, attrs_tuple[0], username) GnomeKeyring.Attribute.list_append_string(attrs, attrs_tuple[1], service) result, items = GnomeKeyring.find_items_sync( GnomeKeyring.ItemType.NETWORK_PASSWORD, attrs ) if result == GnomeKeyring.Result.OK: passwords += items elif deleting: if result == GnomeKeyring.Result.CANCELLED: raise PasswordDeleteError("Cancelled by user") elif result != GnomeKeyring.Result.NO_MATCH: raise PasswordDeleteError(result.value_name) return passwords def get_password(self, service, username): """Get password of the username for the service""" items = self._find_passwords(service, username) if not items: return None secret = items[0].secret return secret if isinstance(secret, str) else secret.decode('utf-8') def set_password(self, service, username, password): """Set password for the username of the service""" service = self._safe_string(service) username = self._safe_string(username) password = self._safe_string(password) attrs = GnomeKeyring.Attribute.list_new() GnomeKeyring.Attribute.list_append_string(attrs, 'username', username) GnomeKeyring.Attribute.list_append_string(attrs, 'service', service) GnomeKeyring.Attribute.list_append_string( attrs, 'application', 'python-keyring' ) result = GnomeKeyring.item_create_sync( self.keyring_name, GnomeKeyring.ItemType.NETWORK_PASSWORD, "Password for '%s' on '%s'" % (username, service), attrs, password, True, )[0] if result == GnomeKeyring.Result.CANCELLED: # The user pressed "Cancel" when prompted to unlock their keyring. raise PasswordSetError("Cancelled by user") elif result != GnomeKeyring.Result.OK: raise PasswordSetError(result.value_name) def delete_password(self, service, username): """Delete the password for the username of the service.""" items = self._find_passwords(service, username, deleting=True) if not items: raise PasswordDeleteError("Password not found") for current in items: result = GnomeKeyring.item_delete_sync(current.keyring, current.item_id) if result == GnomeKeyring.Result.CANCELLED: raise PasswordDeleteError("Cancelled by user") elif result != GnomeKeyring.Result.OK: raise PasswordDeleteError(result.value_name) def _safe_string(self, source, encoding='utf-8'): """Convert unicode to string as gnomekeyring barfs on unicode""" if not isinstance(source, str): return source.encode(encoding) return str(source) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/Google.py0000644000175100001730000003021414457340770017743 0ustar00runnerdockerfrom __future__ import absolute_import import os import sys import copy import codecs import base64 import io import pickle try: import gdata.docs.service except ImportError: pass from jaraco.classes import properties from . import keyczar from keyring import errors from keyring import credentials from keyring.backend import KeyringBackend from keyring.errors import ExceptionRaisedContext class EnvironCredential(credentials.EnvironCredential): """Retrieve credentials from specifically named environment variables""" def __init__(self): super(EnvironCredential, self).__init__( 'GOOGLE_KEYRING_USER', 'GOOGLE_KEYRING_PASSWORD' ) class DocsKeyring(KeyringBackend): """Backend that stores keyring on Google Docs. Note that login and any other initialisation is deferred until it is actually required to allow this keyring class to be added to the global _all_keyring list. """ keyring_title = 'GoogleKeyring' # status enums OK = 1 FAIL = 0 CONFLICT = -1 def __init__( self, credential, source, crypter, collection=None, client=None, can_create=True, input_getter=input, ): self.credential = credential self.crypter = crypter self.source = source self._collection = collection self.can_create = can_create self.input_getter = input_getter self._keyring_dict = None if not client: self._client = gdata.docs.service.DocsService() else: self._client = client self._client.source = source self._client.ssl = True self._login_reqd = True @properties.classproperty def priority(cls): if not cls._has_gdata(): raise RuntimeError("Requires gdata") if not keyczar.has_keyczar(): raise RuntimeError("Requires keyczar") return 3 @classmethod def _has_gdata(cls): with ExceptionRaisedContext() as exc: gdata.__name__ return not bool(exc) def get_password(self, service, username): """Get password of the username for the service""" result = self._get_entry(self._keyring, service, username) if result: result = self._decrypt(result) return result def set_password(self, service, username, password): """Set password for the username of the service""" password = self._encrypt(password or '') keyring_working_copy = copy.deepcopy(self._keyring) service_entries = keyring_working_copy.get(service) if not service_entries: service_entries = {} keyring_working_copy[service] = service_entries service_entries[username] = password save_result = self._save_keyring(keyring_working_copy) if save_result == self.OK: self._keyring_dict = keyring_working_copy return elif save_result == self.CONFLICT: # check if we can avoid updating self.docs_entry, keyring_dict = self._read() existing_pwd = self._get_entry(self._keyring, service, username) conflicting_pwd = self._get_entry(keyring_dict, service, username) if conflicting_pwd == password: # if someone else updated it to the same value then we are done self._keyring_dict = keyring_working_copy return elif conflicting_pwd is None or conflicting_pwd == existing_pwd: # if doesn't already exist or is unchanged then update it new_service_entries = keyring_dict.get(service, {}) new_service_entries[username] = password keyring_dict[service] = new_service_entries save_result = self._save_keyring(keyring_dict) if save_result == self.OK: self._keyring_dict = keyring_dict return else: raise errors.PasswordSetError( 'Failed write after conflict detected' ) else: raise errors.PasswordSetError( 'Conflict detected, service:%s and username:%s was ' 'set to a different value by someone else' % (service, username) ) raise errors.PasswordSetError('Could not save keyring') def delete_password(self, service, username): return self._del_entry(self._keyring, service, username) @property def client(self): if not self._client.GetClientLoginToken(): try: self._client.ClientLogin( self.credential.username, self.credential.password, self._client.source, ) except gdata.service.CaptchaRequired: sys.stdout.write('Please visit ' + self._client.captcha_url) answer = self.input_getter('Answer to the challenge? ') self._client.email = self.credential.username self._client.password = self.credential.password self._client.ClientLogin( self.credential.username, self.credential.password, self._client.source, captcha_token=self._client.captcha_token, captcha_response=answer, ) except gdata.service.BadAuthentication: raise errors.InitError('Users credential were unrecognized') except gdata.service.Error: raise errors.InitError('Login Error') return self._client @property def collection(self): return self._collection or self.credential.username.split('@')[0] @property def _keyring(self): if self._keyring_dict is None: self.docs_entry, self._keyring_dict = self._read() return self._keyring_dict def _get_entry(self, keyring_dict, service, username): result = None service_entries = keyring_dict.get(service) if service_entries: result = service_entries.get(username) return result def _del_entry(self, keyring_dict, service, username): service_entries = keyring_dict.get(service) if not service_entries: raise errors.PasswordDeleteError("No matching service") try: del service_entries[username] except KeyError: raise errors.PasswordDeleteError("Not found") if not service_entries: del keyring_dict[service] def _decrypt(self, value): if not value: return '' return self.crypter.decrypt(value) def _encrypt(self, value): if not value: return '' return self.crypter.encrypt(value) def _get_doc_title(self): return '%s' % self.keyring_title def _read(self): from gdata.docs.service import DocumentQuery title_query = DocumentQuery(categories=[self.collection]) title_query['title'] = self._get_doc_title() title_query['title-exact'] = 'true' docs = self.client.QueryDocumentListFeed(title_query.ToUri()) if not docs.entry: if self.can_create: docs_entry = None keyring_dict = {} else: raise errors.InitError( '%s not found in %s and create not permitted' % (self._get_doc_title(), self.collection) ) else: docs_entry = docs.entry[0] file_contents = '' try: url = docs_entry.content.src url += '&exportFormat=txt' server_response = self.client.request('GET', url) if server_response.status != 200: raise errors.InitError( 'Could not read existing Google Docs keyring' ) file_contents = server_response.read() if file_contents.startswith(codecs.BOM_UTF8): file_contents = file_contents[len(codecs.BOM_UTF8) :] keyring_dict = pickle.loads( base64.urlsafe_b64decode(file_contents.decode('string-escape')) ) except pickle.UnpicklingError as ex: raise errors.InitError( 'Could not unpickle existing Google Docs keyring', ex ) except TypeError as ex: raise errors.InitError( 'Could not decode existing Google Docs keyring', ex ) return docs_entry, keyring_dict def _save_keyring(self, keyring_dict): """Helper to actually write the keyring to Google""" import gdata result = self.OK file_contents = base64.urlsafe_b64encode(pickle.dumps(keyring_dict)) try: if self.docs_entry: extra_headers = { 'Content-Type': 'text/plain', 'Content-Length': len(file_contents), } self.docs_entry = self.client.Put( file_contents, self.docs_entry.GetEditMediaLink().href, extra_headers=extra_headers, ) else: from gdata.docs.service import DocumentQuery # check for existence of folder, create if required folder_query = DocumentQuery(categories=['folder']) folder_query['title'] = self.collection folder_query['title-exact'] = 'true' docs = self.client.QueryDocumentListFeed(folder_query.ToUri()) if docs.entry: folder_entry = docs.entry[0] else: folder_entry = self.client.CreateFolder(self.collection) file_handle = io.BytesIO(file_contents) media_source = gdata.MediaSource( file_handle=file_handle, content_type='text/plain', content_length=len(file_contents), file_name='temp', ) self.docs_entry = self.client.Upload( media_source, self._get_doc_title(), folder_or_uri=folder_entry ) except gdata.service.RequestError as ex: try: if ex.message['reason'].lower().find('conflict') != -1: result = self.CONFLICT else: # Google docs has a bug when updating a shared document # using PUT from any account other that the owner. # It returns an error 400 "Sorry, there was an error saving # the file. Please try again" # *despite* actually updating the document! # Workaround by re-reading to see if it actually updated msg = 'Sorry, there was an error saving the file' if ex.message['body'].find(msg) != -1: new_docs_entry, new_keyring_dict = self._read() if new_keyring_dict == keyring_dict: result = self.OK else: result = self.FAIL else: result = self.FAIL except Exception: result = self.FAIL return result class KeyczarDocsKeyring(DocsKeyring): """Google Docs keyring using keyczar initialized from environment variables """ def __init__(self): crypter = keyczar.EnvironCrypter() credential = EnvironCredential() source = os.environ.get('GOOGLE_KEYRING_SOURCE') super(KeyczarDocsKeyring, self).__init__(credential, source, crypter) def supported(self): """ Return if this keyring supports current environment: -1: not applicable 0: suitable 1: recommended """ try: __import__('keyczar.keyczar') return super(KeyczarDocsKeyring, self).supported() except ImportError: return -1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/Windows.py0000644000175100001730000001113514457340770020162 0ustar00runnerdockerimport sys import base64 import platform from jaraco.classes import properties from keyring.backend import KeyringBackend from keyring.errors import PasswordDeleteError, ExceptionRaisedContext from . import file_base try: import winreg except ImportError: pass try: from . import _win_crypto except ImportError: pass def has_wincrypto(): """ Does this environment have wincrypto? Should return False even when Mercurial's Demand Import allowed import of _win_crypto, so accesses an attribute of the module. """ with ExceptionRaisedContext() as exc: _win_crypto.__name__ return not bool(exc) class EncryptedKeyring(file_base.Keyring): """ A File-based keyring secured by Windows Crypto API. """ version = "1.0" @properties.classproperty def priority(self): """ Preferred over file.EncryptedKeyring but not other, more sophisticated Windows backends. """ if not platform.system() == 'Windows': raise RuntimeError("Requires Windows") return 0.8 filename = 'wincrypto_pass.cfg' def encrypt(self, password, assoc=None): """Encrypt the password using the CryptAPI.""" return _win_crypto.encrypt(password) def decrypt(self, password_encrypted, assoc=None): """Decrypt the password using the CryptAPI.""" return _win_crypto.decrypt(password_encrypted) class RegistryKeyring(KeyringBackend): """ RegistryKeyring is a keyring which use Windows CryptAPI to encrypt the user's passwords and store them under registry keys """ @properties.classproperty def priority(self): """ Preferred on Windows when pywin32 isn't installed """ if platform.system() != 'Windows': raise RuntimeError("Requires Windows") if not has_wincrypto(): raise RuntimeError("Requires ctypes") return 2 @staticmethod def _key_for_service(service): escaped = service.replace('\\', '__0x5c__') return r'Software\{escaped}\Keyring'.format(**locals()) def get_password(self, service, username): """Get password of the username for the service""" try: # fetch the password key = self._key_for_service(service) hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key) password_saved = winreg.QueryValueEx(hkey, username)[0] password_base64 = password_saved.encode('ascii') # decode with base64 password_encrypted = base64.decodebytes(password_base64) # decrypted the password password = _win_crypto.decrypt(password_encrypted).decode('utf-8') except EnvironmentError: password = None return password def set_password(self, service, username, password): """Write the password to the registry""" # encrypt the password password_encrypted = _win_crypto.encrypt(password.encode('utf-8')) # encode with base64 password_base64 = base64.encodebytes(password_encrypted) # encode again to unicode password_saved = password_base64.decode('ascii') # store the password key_name = self._key_for_service(service) hkey = winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_name) winreg.SetValueEx(hkey, username, 0, winreg.REG_SZ, password_saved) def delete_password(self, service, username): """Delete the password for the username of the service.""" try: key_name = self._key_for_service(service) hkey = winreg.OpenKey( winreg.HKEY_CURRENT_USER, key_name, 0, winreg.KEY_ALL_ACCESS ) winreg.DeleteValue(hkey, username) winreg.CloseKey(hkey) except WindowsError: e = sys.exc_info()[1] raise PasswordDeleteError(e) self._delete_key_if_empty(service) def _delete_key_if_empty(self, service): key_name = self._key_for_service(service) key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, key_name, 0, winreg.KEY_ALL_ACCESS ) try: winreg.EnumValue(key, 0) return except WindowsError: pass winreg.CloseKey(key) # it's empty; delete everything while key_name != 'Software': parent, sep, base = key_name.rpartition('\\') key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, parent, 0, winreg.KEY_ALL_ACCESS ) winreg.DeleteKey(key, base) winreg.CloseKey(key) key_name = parent ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/__init__.py0000644000175100001730000000000014457340770020254 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/_win_crypto.py0000644000175100001730000000553014457340770021066 0ustar00runnerdockerfrom __future__ import unicode_literals from ctypes import ( Structure, POINTER, c_void_p, cast, create_string_buffer, c_char_p, byref, memmove, ) from ctypes import windll, WinDLL, WINFUNCTYPE try: from ctypes import wintypes except ValueError: # see http://bugs.python.org/issue16396 raise ImportError("wintypes") # Crypto API ctypes bindings class DATA_BLOB(Structure): _fields_ = [('cbData', wintypes.DWORD), ('pbData', POINTER(wintypes.BYTE))] class CRYPTPROTECT_PROMPTSTRUCT(Structure): _fields_ = [ ('cbSize', wintypes.DWORD), ('dwPromptFlags', wintypes.DWORD), ('hwndApp', wintypes.HWND), ('szPrompt', POINTER(wintypes.WCHAR)), ] # Flags for CRYPTPROTECT_PROMPTSTRUCT CRYPTPROTECT_PROMPT_ON_UNPROTECT = 1 CRYPTPROTECT_PROMPT_ON_PROTECT = 2 # Flags for CryptProtectData/CryptUnprotectData CRYPTPROTECT_UI_FORBIDDEN = 0x01 CRYPTPROTECT_LOCAL_MACHINE = 0x04 CRYPTPROTECT_CRED_SYNC = 0x08 CRYPTPROTECT_AUDIT = 0x10 CRYPTPROTECT_NO_RECOVERY = 0x20 CRYPTPROTECT_VERIFY_PROTECTION = 0x40 CRYPTPROTECT_CRED_REGENERATE = 0x80 # Crypto API Functions _dll = WinDLL('CRYPT32.DLL') CryptProtectData = WINFUNCTYPE( wintypes.BOOL, POINTER(DATA_BLOB), POINTER(wintypes.WCHAR), POINTER(DATA_BLOB), c_void_p, POINTER(CRYPTPROTECT_PROMPTSTRUCT), wintypes.DWORD, POINTER(DATA_BLOB), )(('CryptProtectData', _dll)) CryptUnprotectData = WINFUNCTYPE( wintypes.BOOL, POINTER(DATA_BLOB), POINTER(wintypes.WCHAR), POINTER(DATA_BLOB), c_void_p, POINTER(CRYPTPROTECT_PROMPTSTRUCT), wintypes.DWORD, POINTER(DATA_BLOB), )(('CryptUnprotectData', _dll)) # Functions def encrypt(data, non_interactive=0): blobin = DATA_BLOB( cbData=len(data), pbData=cast(c_char_p(data), POINTER(wintypes.BYTE)) ) blobout = DATA_BLOB() if not CryptProtectData( byref(blobin), 'python-keyring-lib.win32crypto', None, None, None, CRYPTPROTECT_UI_FORBIDDEN, byref(blobout), ): raise OSError("Can't encrypt") encrypted = create_string_buffer(blobout.cbData) memmove(encrypted, blobout.pbData, blobout.cbData) windll.kernel32.LocalFree(blobout.pbData) return encrypted.raw def decrypt(encrypted, non_interactive=0): blobin = DATA_BLOB( cbData=len(encrypted), pbData=cast(c_char_p(encrypted), POINTER(wintypes.BYTE)) ) blobout = DATA_BLOB() if not CryptUnprotectData( byref(blobin), 'python-keyring-lib.win32crypto', None, None, None, CRYPTPROTECT_UI_FORBIDDEN, byref(blobout), ): raise OSError("Can't decrypt") data = create_string_buffer(blobout.cbData) memmove(data, blobout.pbData, blobout.cbData) windll.kernel32.LocalFree(blobout.pbData) return data.raw ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/escape.py0000644000175100001730000000220114457340770017762 0ustar00runnerdocker""" escape/unescape routines available for backends which need alphanumeric usernames, services, or other values """ import re import string LEGAL_CHARS = ( getattr(string, 'letters', None) # Python 2 or getattr(string, 'ascii_letters') # Python 3 ) + string.digits ESCAPE_FMT = "_%02X" def _escape_char(c): "Single char escape. Return the char, escaped if not already legal" if isinstance(c, int): c = chr(c) return c if c in LEGAL_CHARS else ESCAPE_FMT % ord(c) def escape(value): """ Escapes given string so the result consists of alphanumeric chars and underscore only. """ return "".join(_escape_char(c) for c in value.encode('utf-8')) def _unescape_code(regex_match): ordinal = int(regex_match.group('code'), 16) return bytes([ordinal]) def unescape(value): """ Inverse of escape. """ pattern = ESCAPE_FMT.replace('%02X', '(?P[0-9A-Fa-f]{2})') # the pattern must be bytes to operate on bytes pattern_bytes = pattern.encode('ascii') re_esc = re.compile(pattern_bytes) return re_esc.sub(_unescape_code, value.encode('ascii')).decode('utf-8') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/file.py0000644000175100001730000001705114457340770017452 0ustar00runnerdockerfrom __future__ import with_statement import os import sys import json import getpass import configparser from jaraco.classes import properties from .escape import escape as escape_for_ini from keyrings.alt.file_base import Keyring, decodebytes, encodebytes class PlaintextKeyring(Keyring): """Simple File Keyring with no encryption""" priority = 0.5 # type: ignore "Applicable for all platforms, but not recommended" filename = 'keyring_pass.cfg' scheme = 'no encyption' version = '1.0' def encrypt(self, password, assoc=None): """Directly return the password itself, ignore associated data.""" return password def decrypt(self, password_encrypted, assoc=None): """Directly return encrypted password, ignore associated data.""" return password_encrypted class Encrypted: """ PyCryptodome-backed Encryption support """ scheme = '[PBKDF2] AES256.CFB' version = '1.0' block_size = 32 def __init__(self): vars(self).update(self._get_crypto_impl()) @staticmethod def _get_crypto_impl(): try: from Cryptodome.Protocol import KDF # noqa: F401 from Cryptodome.Cipher import AES # noqa: F401 import Cryptodome.Random as Random # noqa: F401 except ImportError: from Crypto.Protocol import KDF # noqa: F401 from Crypto.Cipher import AES # noqa: F401 import Crypto.Random as Random # noqa: F401 return locals() def _create_cipher(self, password, salt, IV): """ Create the cipher object to encrypt or decrypt a payload. """ pw = self.KDF.PBKDF2(password, salt, dkLen=self.block_size) return self.AES.new(pw[: self.block_size], self.AES.MODE_CFB, IV) def _get_new_password(self): while True: password = getpass.getpass("Please set a password for your new keyring: ") confirm = getpass.getpass('Please confirm the password: ') if password != confirm: # pragma: no cover sys.stderr.write("Error: Your passwords didn't match\n") continue if '' == password.strip(): # pragma: no cover # forbid the blank password sys.stderr.write("Error: blank passwords aren't allowed.\n") continue return password class EncryptedKeyring(Encrypted, Keyring): """PyCryptodome File Keyring""" filename = 'crypted_pass.cfg' pw_prefix = 'pw:'.encode() @properties.classproperty def priority(cls): "Applicable for all platforms, but not recommended." try: cls._get_crypto_impl() except ImportError: # pragma: no cover raise RuntimeError("pycryptodome/x required") if not json: # pragma: no cover raise RuntimeError("JSON implementation such as simplejson required.") return 0.6 @properties.NonDataProperty def keyring_key(self): # _unlock or _init_file will set the key or raise an exception if self._check_file(): self._unlock() else: self._init_file() return self.keyring_key def _init_file(self): """ Initialize a new password file and set the reference password. """ self.keyring_key = self._get_new_password() # set a reference password, used to check that the password provided # matches for subsequent checks. self.set_password( 'keyring-setting', 'password reference', 'password reference value' ) self._write_config_value('keyring-setting', 'scheme', self.scheme) self._write_config_value('keyring-setting', 'version', self.version) def _check_file(self): """ Check if the file exists and has the expected password reference. """ if not os.path.exists(self.file_path): return False self._migrate() config = configparser.RawConfigParser() config.read(self.file_path) try: config.get( escape_for_ini('keyring-setting'), escape_for_ini('password reference') ) except (configparser.NoSectionError, configparser.NoOptionError): return False try: self._check_scheme(config) except AttributeError: # accept a missing scheme return True return self._check_version(config) def _check_scheme(self, config): """ check for a valid scheme raise ValueError otherwise raise AttributeError if missing """ try: scheme = config.get( escape_for_ini('keyring-setting'), escape_for_ini('scheme') ) except (configparser.NoSectionError, configparser.NoOptionError): raise AttributeError("Encryption scheme missing") # remove pointless crypto module name if scheme.startswith('PyCrypto '): scheme = scheme[9:] if scheme != self.scheme: raise ValueError( "Encryption scheme mismatch " "(exp.: %s, found: %s)" % (self.scheme, scheme) ) def _check_version(self, config): """ check for a valid version an existing scheme implies an existing version as well return True, if version is valid, and False otherwise """ try: self.file_version = config.get( escape_for_ini('keyring-setting'), escape_for_ini('version') ) except (configparser.NoSectionError, configparser.NoOptionError): return False return True def _unlock(self): """ Unlock this keyring by getting the password for the keyring from the user. """ self.keyring_key = getpass.getpass( 'Please enter password for encrypted keyring: ' ) try: ref_pw = self.get_password('keyring-setting', 'password reference') assert ref_pw == 'password reference value' except AssertionError: self._lock() raise ValueError("Incorrect Password") def _lock(self): """ Remove the keyring key from this instance. """ del self.keyring_key def encrypt(self, password, assoc=None): # encrypt password, ignore associated data salt = self.Random.get_random_bytes(self.block_size) IV = self.Random.get_random_bytes(self.AES.block_size) cipher = self._create_cipher(self.keyring_key, salt, IV) password_encrypted = cipher.encrypt(self.pw_prefix + password) # Serialize the salt, IV, and encrypted password in a secure format data = dict(salt=salt, IV=IV, password_encrypted=password_encrypted) for key in data: # spare a few bytes: throw away newline from base64 encoding data[key] = encodebytes(data[key]).decode()[:-1] return json.dumps(data).encode() def decrypt(self, password_encrypted, assoc=None): # unpack the encrypted payload, ignore associated data data = json.loads(password_encrypted.decode()) for key in data: data[key] = decodebytes(data[key].encode()) cipher = self._create_cipher(self.keyring_key, data['salt'], data['IV']) plaintext = cipher.decrypt(data['password_encrypted']) assert plaintext.startswith(self.pw_prefix) return plaintext[3:] def _migrate(self, keyring_password=None): """ Convert older keyrings to the current format. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/file_base.py0000644000175100001730000001442314457340770020444 0ustar00runnerdockerfrom __future__ import with_statement import os import abc import configparser from base64 import encodebytes, decodebytes from jaraco.classes import properties from keyring.errors import PasswordDeleteError from keyring.backend import KeyringBackend from keyring.util import platform_ from .escape import escape as escape_for_ini class FileBacked: @abc.abstractproperty def filename(self): """ The filename used to store the passwords. """ @properties.NonDataProperty def file_path(self): """ The path to the file where passwords are stored. This property may be overridden by the subclass or at the instance level. """ return os.path.join(platform_.data_root(), self.filename) @abc.abstractproperty def scheme(self): """ The encryption scheme used to store the passwords. """ return 'not defined' @abc.abstractproperty def version(self): """ The encryption version used to store the passwords. """ return None @properties.NonDataProperty def file_version(self): """ The encryption version used in file to store the passwords. """ return None def __repr__(self): tmpl = ( "<{self.__class__.__name__} with {self.scheme} " "v.{self.version} at {self.file_path}>" ) return tmpl.format(**locals()) class Keyring(FileBacked, KeyringBackend): """ BaseKeyring is a file-based implementation of keyring. This keyring stores the password directly in the file and provides methods which may be overridden by subclasses to support encryption and decryption. The encrypted payload is stored in base64 format. """ @abc.abstractmethod def encrypt(self, password, assoc=None): """ Given a password (byte string) and assoc (byte string, optional), return an encrypted byte string. assoc provides associated data (typically: service and username) """ @abc.abstractmethod def decrypt(self, password_encrypted, assoc=None): """ Given a password encrypted by a previous call to `encrypt`, and assoc (byte string, optional), return the original byte string. assoc provides associated data (typically: service and username) """ def get_password(self, service, username): """ Read the password from the file. """ assoc = self._generate_assoc(service, username) service = escape_for_ini(service) username = escape_for_ini(username) # load the passwords from the file config = configparser.RawConfigParser() if os.path.exists(self.file_path): config.read(self.file_path) # fetch the password try: password_base64 = config.get(service, username).encode() # decode with base64 password_encrypted = decodebytes(password_base64) # decrypt the password with associated data try: password = self.decrypt(password_encrypted, assoc).decode('utf-8') except ValueError: # decrypt the password without associated data password = self.decrypt(password_encrypted).decode('utf-8') except (configparser.NoOptionError, configparser.NoSectionError): password = None return password def set_password(self, service, username, password): """Write the password in the file.""" if not username: # https://github.com/jaraco/keyrings.alt/issues/21 raise ValueError("Username cannot be blank.") if not isinstance(password, str): raise TypeError("Password should be a unicode string, not bytes.") assoc = self._generate_assoc(service, username) # encrypt the password password_encrypted = self.encrypt(password.encode('utf-8'), assoc) # encode with base64 and add line break to untangle config file password_base64 = '\n' + encodebytes(password_encrypted).decode() self._write_config_value(service, username, password_base64) def _generate_assoc(self, service, username): """Generate tamper resistant bytestring of associated data""" return (escape_for_ini(service) + r'\0' + escape_for_ini(username)).encode() def _write_config_value(self, service, key, value): # ensure the file exists self._ensure_file_path() # load the keyring from the disk config = configparser.RawConfigParser() config.read(self.file_path) service = escape_for_ini(service) key = escape_for_ini(key) # update the keyring with the password if not config.has_section(service): config.add_section(service) config.set(service, key, value) # save the keyring back to the file with open(self.file_path, 'w') as config_file: config.write(config_file) def _ensure_file_path(self): """ Ensure the storage path exists. If it doesn't, create it with "go-rwx" permissions. """ storage_root = os.path.dirname(self.file_path) needs_storage_root = storage_root and not os.path.isdir(storage_root) if needs_storage_root: # pragma: no cover os.makedirs(storage_root) if not os.path.isfile(self.file_path): # create the file without group/world permissions with open(self.file_path, 'w'): pass user_read_write = 0o600 os.chmod(self.file_path, user_read_write) def delete_password(self, service, username): """Delete the password for the username of the service.""" service = escape_for_ini(service) username = escape_for_ini(username) config = configparser.RawConfigParser() if os.path.exists(self.file_path): config.read(self.file_path) try: if not config.remove_option(service, username): raise PasswordDeleteError("Password not found") except configparser.NoSectionError: raise PasswordDeleteError("Password not found") # update the file with open(self.file_path, 'w') as config_file: config.write(config_file) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/keyczar.py0000644000175100001730000000544514457340770020207 0ustar00runnerdockerfrom __future__ import absolute_import import os import abc try: from keyczar import keyczar except ImportError: pass import keyring.backend from keyring import errors def has_keyczar(): with errors.ExceptionRaisedContext() as exc: keyczar.__name__ return not bool(exc) class BaseCrypter(keyring.backend.Crypter): """Base Keyczar keyset encryption and decryption. The keyset initialisation is deferred until required. """ @abc.abstractproperty def keyset_location(self): """Location for the main keyset that may be encrypted or not""" pass @abc.abstractproperty def encrypting_keyset_location(self): """Location for the encrypting keyset. Use None to indicate that the main keyset is not encrypted """ pass @property def crypter(self): """The actual keyczar crypter""" if not hasattr(self, '_crypter'): # initialise the Keyczar keysets if not self.keyset_location: raise ValueError('No encrypted keyset location!') reader = keyczar.readers.CreateReader(self.keyset_location) if self.encrypting_keyset_location: encrypting_keyczar = keyczar.Crypter.Read( self.encrypting_keyset_location ) reader = keyczar.readers.EncryptedReader(reader, encrypting_keyczar) self._crypter = keyczar.Crypter(reader) return self._crypter def encrypt(self, value): """Encrypt the value.""" if not value: return '' return self.crypter.Encrypt(value) def decrypt(self, value): """Decrypt the value.""" if not value: return '' return self.crypter.Decrypt(value) class Crypter(BaseCrypter): """A Keyczar crypter using locations specified in the constructor""" def __init__(self, keyset_location, encrypting_keyset_location=None): self._keyset_location = keyset_location self._encrypting_keyset_location = encrypting_keyset_location @property def keyset_location(self): return self._keyset_location @property def encrypting_keyset_location(self): return self._encrypting_keyset_location class EnvironCrypter(BaseCrypter): """A Keyczar crypter using locations specified by environment vars""" KEYSET_ENV_VAR = 'KEYRING_KEYCZAR_ENCRYPTED_LOCATION' ENC_KEYSET_ENV_VAR = 'KEYRING_KEYCZAR_ENCRYPTING_LOCATION' @property def keyset_location(self): val = os.environ.get(self.KEYSET_ENV_VAR) if not val: raise ValueError('%s environment value not set' % self.KEYSET_ENV_VAR) return val @property def encrypting_keyset_location(self): return os.environ.get(self.ENC_KEYSET_ENV_VAR) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/keyrings/alt/multi.py0000644000175100001730000000415214457340770017663 0ustar00runnerdockerimport itertools from jaraco.classes import properties from keyring.backend import KeyringBackend from keyring import errors class MultipartKeyringWrapper(KeyringBackend): """A wrapper around an existing keyring that breaks the password into smaller parts to handle implementations that have limits on the maximum length of passwords i.e. Windows Vault """ def __init__(self, keyring, max_password_size=512): self._keyring = keyring self._max_password_size = max_password_size @properties.classproperty def priority(cls): return 0 def get_password(self, service, username): """Get password of the username for the service""" init_part = self._keyring.get_password(service, username) if init_part: parts = [init_part] i = 1 while True: next_part = self._keyring.get_password( service, '%s{{part_%d}}' % (username, i) ) if next_part: parts.append(next_part) i += 1 else: break return ''.join(parts) return None def set_password(self, service, username, password): """Set password for the username of the service""" segments = range(0, len(password), self._max_password_size) password_parts = [password[i : i + self._max_password_size] for i in segments] for i, password_part in enumerate(password_parts): curr_username = username if i > 0: curr_username += '{{part_%d}}' % i self._keyring.set_password(service, curr_username, password_part) def delete_password(self, service, username): self._keyring.delete_password(service, username) count = itertools.count(1) while True: part_name = '%(username)s{{part_%(index)d}}' % dict( index=next(count), **vars() ) try: self._keyring.delete_password(service, part_name) except errors.PasswordDeleteError: break ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6698575 keyrings.alt-5.0.0/keyrings.alt.egg-info/0000755000175100001730000000000014457341023017636 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/PKG-INFO0000644000175100001730000000516314457341022020737 0ustar00runnerdockerMetadata-Version: 2.1 Name: keyrings.alt Version: 5.0.0 Summary: Alternate keyring implementations Home-page: https://github.com/jaraco/keyrings.alt Author: Jason R. Coombs Author-email: jaraco@jaraco.com Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Requires-Python: >=3.8 Provides-Extra: testing Provides-Extra: docs License-File: LICENSE .. image:: https://img.shields.io/pypi/v/keyrings.alt.svg :target: https://pypi.org/project/keyrings.alt .. image:: https://img.shields.io/pypi/pyversions/keyrings.alt.svg .. image:: https://github.com/jaraco/keyrings.alt/workflows/tests/badge.svg :target: https://github.com/jaraco/keyrings.alt/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/keyrings.alt :target: https://tidelift.com/subscription/pkg/pypi-keyrings.alt?utm_source=pypi-keyrings.alt&utm_medium=readme Alternate keyring backend implementations for use with the `keyring package `_. Keyrings in this package may have security risks or other implications. These backends were extracted from the main keyring project to make them available for those who wish to employ them, but are discouraged for general production use. Include this module and use its backends at your own risk. For example, the PlaintextKeyring stores passwords in plain text on the file system, defeating the intended purpose of this library to encourage best practices for security. For Enterprise ============== Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/SOURCES.txt0000644000175100001730000000167514457341022021532 0ustar00runnerdocker.coveragerc .editorconfig .pre-commit-config.yaml .readthedocs.yaml LICENSE NEWS.rst README.rst SECURITY.md conftest.py mypy.ini pyproject.toml pytest.ini setup.cfg towncrier.toml tox.ini .github/FUNDING.yml .github/dependabot.yml .github/workflows/main.yml docs/conf.py docs/history.rst docs/index.rst keyrings/__init__.py keyrings.alt.egg-info/PKG-INFO keyrings.alt.egg-info/SOURCES.txt keyrings.alt.egg-info/dependency_links.txt keyrings.alt.egg-info/entry_points.txt keyrings.alt.egg-info/requires.txt keyrings.alt.egg-info/top_level.txt keyrings/alt/Gnome.py keyrings/alt/Google.py keyrings/alt/Windows.py keyrings/alt/__init__.py keyrings/alt/_win_crypto.py keyrings/alt/escape.py keyrings/alt/file.py keyrings/alt/file_base.py keyrings/alt/keyczar.py keyrings/alt/multi.py tests/__init__.py tests/mocks.py tests/test_Gnome.py tests/test_Google.py tests/test_Windows.py tests/test_crypto.py tests/test_file.py tests/test_keyczar.py tests/test_multi.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/dependency_links.txt0000644000175100001730000000000114457341022023703 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/entry_points.txt0000644000175100001730000000030314457341022023127 0ustar00runnerdocker[keyring.backends] Gnome = keyrings.alt.Gnome Google = keyrings.alt.Google Windows (alt) = keyrings.alt.Windows file = keyrings.alt.file keyczar = keyrings.alt.keyczar multi = keyrings.alt.multi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/requires.txt0000644000175100001730000000062714457341022022242 0ustar00runnerdockerjaraco.classes [docs] sphinx>=3.5 jaraco.packaging>=9.3 rst.linker>=1.9 furo sphinx-lint jaraco.tidelift>=1.4 [testing] pytest>=6 pytest-checkdocs>=2.4 pytest-cov pytest-enabler>=2.2 pytest-ruff backports.unittest_mock keyring>=20 pycryptodomex pycryptodome [testing:platform_python_implementation != "PyPy"] pytest-black>=0.3.7 pytest-mypy>=0.9.1 [testing:python_version == "2.7"] gdata python-keyczar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157586.0 keyrings.alt-5.0.0/keyrings.alt.egg-info/top_level.txt0000644000175100001730000000001114457341022022357 0ustar00runnerdockerkeyrings ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/mypy.ini0000644000175100001730000000023214457340770015236 0ustar00runnerdocker[mypy] ignore_missing_imports = True # required to support namespace packages # https://github.com/python/mypy/issues/14057 explicit_package_bases = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/pyproject.toml0000644000175100001730000000027214457340770016457 0ustar00runnerdocker[build-system] requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/pytest.ini0000644000175100001730000000151214457340770015572 0ustar00runnerdocker[pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules filterwarnings= ## upstream # Ensure ResourceWarnings are emitted default::ResourceWarning # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy # python/cpython#100750 ignore:'encoding' argument not specified::platform # pypa/build#615 ignore:'encoding' argument not specified::build.env ## end upstream ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6738575 keyrings.alt-5.0.0/setup.cfg0000644000175100001730000000255314457341023015360 0ustar00runnerdocker[metadata] name = keyrings.alt author = Jason R. Coombs author_email = jaraco@jaraco.com description = Alternate keyring implementations long_description = file:README.rst url = https://github.com/jaraco/keyrings.alt classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only [options] packages = find_namespace: include_package_data = true python_requires = >=3.8 install_requires = jaraco.classes [options.packages.find] exclude = build* dist* docs* tests* [options.extras_require] testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-black >= 0.3.7; \ python_implementation != "PyPy" pytest-cov pytest-mypy >= 0.9.1; \ python_implementation != "PyPy" pytest-enabler >= 2.2 pytest-ruff backports.unittest_mock keyring >= 20 pycryptodomex pycryptodome gdata; python_version=="2.7" python-keyczar; python_version=="2.7" docs = sphinx >= 3.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo sphinx-lint jaraco.tidelift >= 1.4 [options.entry_points] keyring.backends = file = keyrings.alt.file Gnome = keyrings.alt.Gnome Google = keyrings.alt.Google keyczar = keyrings.alt.keyczar multi = keyrings.alt.multi Windows (alt) = keyrings.alt.Windows [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1690157586.6738575 keyrings.alt-5.0.0/tests/0000755000175100001730000000000014457341023014674 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/__init__.py0000644000175100001730000000000014457340770017003 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/mocks.py0000644000175100001730000001434614457340770016402 0ustar00runnerdocker""" Various mock objects for testing """ import base64 import io import pickle class MockAtom: """Mocks an atom in the GData service.""" def __init__(self, value): self.text = value class MockEntry: """Mocks and entry returned from the GData service.""" def __init__(self, title, ID): self.title = MockAtom(title) self.id = MockAtom('http://mock.example.com/%s' % ID) self.ID = ID # simpler lookup for key value def GetEditMediaLink(self): return MockLink() class MockHTTPClient: """Mocks the functionality of an http client.""" def request(*args, **kwargs): pass class MockGDataService: """Provides the common functionality of a Google Service.""" http_client = MockHTTPClient() def __init__( self, email=None, password=None, account_type='HOSTED_OR_GOOGLE', service=None, auth_service_url=None, source=None, server=None, additional_headers=None, handler=None, tokens=None, http_client=None, token_store=None, ): """Create the Service with the default parameters.""" self.email = email self.password = password self.account_type = account_type self.service = service self.auth_service_url = auth_service_url self.server = server self.login_token = None def GetClientLoginToken(self): return self.login_token def SetClientLoginToken(self, token): self.login_token = token def ClientLogin( self, username, password, account_type=None, service=None, auth_service_url=None, source=None, captcha_token=None, captcha_response=None, ): """Client side login to the service.""" if hasattr(self, '_login_err'): raise self._login_err() class MockDocumentService(MockGDataService): """ Implements the minimum functionality of the Google Document service. """ def Upload(self, media_source, title, folder_or_uri=None, label=None): """ Upload a document. """ if hasattr(self, '_upload_err'): raise self._upload_err() if not hasattr(self, '_upload_count'): self._upload_count = 0 # save the data for asserting against self._upload_data = dict( media_source=media_source, title=title, folder_or_uri=folder_or_uri, label=label, ) self._upload_count += 1 return MockEntry(title, 'mockentry%3A' + title) def QueryDocumentListFeed(self, uri): if hasattr(self, '_listfeed'): return self._listfeed return MockListFeed() def CreateFolder(self, title, folder_or_uri=None): if hasattr(self, '_create_folder_err'): raise self._create_folder_err() if hasattr(self, '_create_folder'): return self._create_folder return MockListEntry() def Put( self, data, uri, extra_headers=None, url_params=None, escape_params=True, redirects_remaining=3, media_source=None, converter=None, ): self._put_data = None if not hasattr(self, '_put_count'): self._put_count = 0 if hasattr(self, '_put_err'): # allow for a list of errors if type(self._put_err) == list: put_err = self._put_err.pop(0) if not len(self._put_err): delattr(self, '_put_err') else: put_err = self._put_err if type(put_err) == tuple: raise put_err[0](put_err[1]) else: raise put_err() # save the data for asserting against assert isinstance(data, str), 'Should be a string' self._put_data = pickle.loads(base64.urlsafe_b64decode(data)) self._put_count += 1 return MockEntry('', 'mockentry%3A' + '') def Export(self, entry_or_id_or_url, file_path, gid=None, extra_params=None): if hasattr(self, '_export_err'): raise self._export_err() if hasattr(self, '_export_data'): export_file = open(file_path, 'wb') export_file.write(self._export_data) export_file.close() def request(self, data, uri): if hasattr(self, '_request_err'): if type(self._request_err) == tuple: raise self._request_err[0](self._request_err[1]) else: raise self._request_err() if hasattr(self, '_request_response'): return MockHttpResponse(self._request_response) class MockHttpResponse(io.BytesIO): def __init__(self, response_dict): super(MockHttpResponse, self).__init__(response_dict.get('data', '')) self.status = response_dict.get('status', 200) self.reason = response_dict.get('reason', '') class MockListFeed: @property def entry(self): if hasattr(self, '_entry'): return self._entry return [] class MockListEntry: pass class MockLink: @property def href(self): return '' class MockContent: @property def src(self): return 'src' class MockDocumentListEntry: @property def content(self): return MockContent() def GetEditMediaLink(self): return MockLink() class MockKeyczarReader: def __init__(self, location): self.location = location class MockKeyczarEncryptedReader: def __init__(self, reader, crypter): self._reader = reader self._crypter = crypter class MockKeyczarReaders: @staticmethod def CreateReader(location): return MockKeyczarReader(location) @staticmethod def EncryptedReader(reader, crypter): return MockKeyczarEncryptedReader(reader, crypter) class MockKeyczarCrypter: def __init__(self, reader): self.reader = reader @staticmethod def Read(location): return MockKeyczarCrypter(MockKeyczarReader(location)) class MockKeyczar: @property def readers(self): return MockKeyczarReaders @property def Crypter(self): return MockKeyczarCrypter ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_Gnome.py0000644000175100001730000000212214457340770017357 0ustar00runnerdockerimport types import sys import unittest from keyring.testing.backend import BackendBasicTests from keyring.testing.util import NoNoneDictMutator from keyrings.alt import Gnome def ImportBlesser(*names, **changes): """A context manager to temporarily make it possible to import a module""" for name in names: changes[name] = types.ModuleType(name) return NoNoneDictMutator(sys.modules, **changes) @unittest.skipUnless(Gnome.Keyring.viable, "Need GnomeKeyring") class GnomeKeyringTestCase(BackendBasicTests, unittest.TestCase): def init_keyring(self): k = Gnome.Keyring() # Store passwords in the session (in-memory) # keyring for the tests. This # is going to be automatically cleared when the user logoff. k.KEYRING_NAME = 'session' return k def test_supported(self): with ImportBlesser('gi.repository'): self.assertTrue(Gnome.Keyring.viable) def test_supported_no_module(self): with NoNoneDictMutator(Gnome.__dict__, GnomeKeyring=None): self.assertFalse(Gnome.Keyring.viable) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_Google.py0000644000175100001730000003566114457340770017544 0ustar00runnerdockerimport codecs import base64 import unittest import pickle from keyring.testing.backend import BackendBasicTests from keyrings.alt import Google from keyring.credentials import SimpleCredential from keyring.backend import NullCrypter from keyring import errors from . import mocks def is_gdata_supported(): try: __import__('gdata.service') except ImportError: return False return True def init_google_docs_keyring(client, can_create=True, input_getter=input): credentials = SimpleCredential('foo', 'bar') return Google.DocsKeyring( credentials, 'test_src', NullCrypter(), client=client, can_create=can_create, input_getter=input_getter, ) @unittest.skipUnless(is_gdata_supported(), "Need Google Docs (gdata)") class GoogleDocsKeyringTestCase(BackendBasicTests, unittest.TestCase): """Run all the standard tests on a new keyring""" def init_keyring(self): client = mocks.MockDocumentService() client.SetClientLoginToken('foo') return init_google_docs_keyring(client) @unittest.skipUnless(is_gdata_supported(), "Need Google Docs (gdata)") class GoogleDocsKeyringInteractionTestCase(unittest.TestCase): """Additional tests for Google Doc interactions""" def _init_client(self, set_token=True): client = mocks.MockDocumentService() if set_token: client.SetClientLoginToken('interaction') return client def _init_keyring(self, client): self.keyring = init_google_docs_keyring(client) def _init_listfeed(self): listfeed = mocks.MockListFeed() listfeed._entry = [mocks.MockDocumentListEntry(), mocks.MockDocumentListEntry()] return listfeed def _encode_data(self, data): return base64.urlsafe_b64encode(pickle.dumps(data)) def test_handles_auth_failure(self): import gdata client = self._init_client(set_token=False) client._login_err = gdata.service.BadAuthentication self._init_keyring(client) with self.assertRaises(errors.InitError): self.keyring.client def test_handles_auth_error(self): import gdata client = self._init_client(set_token=False) client._login_err = gdata.service.Error self._init_keyring(client) with self.assertRaises(errors.InitError): self.keyring.client def test_handles_login_captcha(self): import gdata client = self._init_client(set_token=False) client._login_err = gdata.service.CaptchaRequired client.captcha_url = 'a_captcha_url' client.captcha_token = 'token' self.get_input_called = False def _get_input(prompt): self.get_input_called = True delattr(client, '_login_err') return 'Foo' self.keyring = init_google_docs_keyring(client, input_getter=_get_input) self.keyring.client self.assertTrue(self.get_input_called, 'Should have got input') def test_retrieves_existing_keyring_with_and_without_bom(self): client = self._init_client() dummy_entries = dict(section1=dict(user1='pwd1')) no_utf8_bom_entries = self._encode_data(dummy_entries) client._request_response = dict(status=200, data=no_utf8_bom_entries) client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertEqual(self.keyring.get_password('section1', 'user1'), 'pwd1') utf8_bom_entries = codecs.BOM_UTF8 + no_utf8_bom_entries client._request_response = dict(status=200, data=utf8_bom_entries) self._init_keyring(client) self.assertEqual(self.keyring.get_password('section1', 'user1'), 'pwd1') def test_handles_retrieve_failure(self): client = self._init_client() client._listfeed = self._init_listfeed() client._request_response = dict(status=400, reason='Data centre explosion') self._init_keyring(client) self.assertRaises(errors.InitError, self.keyring.get_password, 'any', 'thing') def test_handles_corrupt_retrieve(self): client = self._init_client() dummy_entries = dict(section1=dict(user1='pwd1')) client._request_response = dict( status=200, data='broken' + self._encode_data(dummy_entries) ) client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertRaises(errors.InitError, self.keyring.get_password, 'any', 'thing') def test_no_create_if_requested(self): client = self._init_client() self.keyring = init_google_docs_keyring(client, can_create=False) self.assertRaises(errors.InitError, self.keyring.get_password, 'any', 'thing') def test_no_set_if_create_folder_fails_on_new_keyring(self): import gdata client = self._init_client() client._create_folder_err = gdata.service.RequestError self._init_keyring(client) self.assertEqual( self.keyring.get_password('service-a', 'user-A'), None, 'No password should be set in new keyring', ) self.assertRaises( errors.PasswordSetError, self.keyring.set_password, 'service-a', 'user-A', 'password-A', ) self.assertEqual( self.keyring.get_password('service-a', 'user-A'), None, 'No password should be set after write fail', ) def test_no_set_if_write_fails_on_new_keyring(self): import gdata client = self._init_client() client._upload_err = gdata.service.RequestError self._init_keyring(client) self.assertEqual( self.keyring.get_password('service-a', 'user-A'), None, 'No password should be set in new keyring', ) self.assertRaises( errors.PasswordSetError, self.keyring.set_password, 'service-a', 'user-A', 'password-A', ) self.assertEqual( self.keyring.get_password('service-a', 'user-A'), None, 'No password should be set after write fail', ) def test_no_set_if_write_fails_on_existing_keyring(self): import gdata client = self._init_client() dummy_entries = dict(sectionB=dict(user9='pwd9')) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = gdata.service.RequestError client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertEqual( self.keyring.get_password('sectionB', 'user9'), 'pwd9', 'Correct password should be set in existing keyring', ) self.assertRaises( errors.PasswordSetError, self.keyring.set_password, 'sectionB', 'user9', 'Not the same pwd', ) self.assertEqual( self.keyring.get_password('sectionB', 'user9'), 'pwd9', 'Password should be unchanged after write fail', ) def test_writes_correct_data_to_google_docs(self): client = self._init_client() dummy_entries = dict(sectionWriteChk=dict(userWriteChk='pwd')) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._listfeed = self._init_listfeed() self._init_keyring(client) self.keyring.set_password('sectionWriteChk', 'userWritechk', 'new_pwd') self.assertIsNotNone(client._put_data, 'Should have written data') self.assertEqual( 'new_pwd', client._put_data.get('sectionWriteChk').get('userWritechk'), 'Did not write updated password!', ) def test_handles_write_conflict_on_different_service(self): import gdata client = self._init_client() dummy_entries = dict( sectionWriteConflictA=dict(userwriteConflictA='pwdwriteConflictA') ) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = [ (gdata.service.RequestError, {'status': '406', 'reason': 'Conflict'}) ] client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertEqual( self.keyring.get_password('sectionWriteConflictA', 'userwriteConflictA'), 'pwdwriteConflictA', 'Correct password should be set in existing keyring', ) dummy_entries['diffSection'] = dict(foo='bar') client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) new_pwd = 'Not the same pwd' self.keyring.set_password( 'sectionWriteConflictA', 'userwriteConflictA', new_pwd ) self.assertEqual( self.keyring.get_password('sectionWriteConflictA', 'userwriteConflictA'), new_pwd, ) self.assertEqual( 1, client._put_count, 'Write not called after conflict resolution' ) def test_handles_write_conflict_on_same_service_and_username(self): import gdata client = self._init_client() dummy_entries = dict( sectionWriteConflictB=dict(userwriteConflictB='pwdwriteConflictB') ) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = ( gdata.service.RequestError, {'status': '406', 'reason': 'Conflict'}, ) client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertEqual( self.keyring.get_password('sectionWriteConflictB', 'userwriteConflictB'), 'pwdwriteConflictB', 'Correct password should be set in existing keyring', ) conflicting_dummy_entries = dict( sectionWriteConflictB=dict(userwriteConflictB='pwdwriteConflictC') ) client._request_response = dict( status=200, data=self._encode_data(conflicting_dummy_entries) ) self.assertRaises( errors.PasswordSetError, self.keyring.set_password, 'sectionWriteConflictB', 'userwriteConflictB', 'new_pwd', ) def test_handles_write_conflict_with_identical_change(self): import gdata client = self._init_client() dummy_entries = dict( sectionWriteConflictC=dict(userwriteConflictC='pwdwriteConflictC') ) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = [ (gdata.service.RequestError, {'status': '406', 'reason': 'Conflict'}) ] client._listfeed = self._init_listfeed() self._init_keyring(client) self.assertEqual( self.keyring.get_password('sectionWriteConflictC', 'userwriteConflictC'), 'pwdwriteConflictC', 'Correct password should be set in existing keyring', ) new_pwd = 'Not the same pwd' conflicting_dummy_entries = dict( sectionWriteConflictC=dict(userwriteConflictC=new_pwd) ) client._request_response = dict( status=200, data=self._encode_data(conflicting_dummy_entries) ) self.keyring.set_password( 'sectionWriteConflictC', 'userwriteConflictC', new_pwd ) self.assertEqual( self.keyring.get_password('sectionWriteConflictC', 'userwriteConflictC'), new_pwd, ) def test_handles_broken_google_put_when_non_owner_update_fails(self): """Google Docs has a bug when putting to a non-owner see GoogleDocsKeyring._save_keyring() """ import gdata client = self._init_client() dummy_entries = dict(sectionBrokenPut=dict(userBrokenPut='pwdBrokenPut')) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = [ ( gdata.service.RequestError, { 'status': '400', 'body': 'Sorry, there was an error saving the ' 'file. Please try again.', 'reason': 'Bad Request', }, ) ] client._listfeed = self._init_listfeed() self._init_keyring(client) new_pwd = 'newPwdBrokenPut' correct_read_entries = dict(sectionBrokenPut=dict(userBrokenPut='pwdBrokenPut')) client._request_response = dict( status=200, data=self._encode_data(correct_read_entries) ) self.assertRaises( errors.PasswordSetError, self.keyring.set_password, 'sectionBrokenPut', 'userBrokenPut', new_pwd, ) def test_handles_broken_google_put_when_non_owner_update(self): """Google Docs has a bug when putting to a non-owner see GoogleDocsKeyring._save_keyring() """ import gdata client = self._init_client() dummy_entries = dict(sectionBrokenPut=dict(userBrokenPut='pwdBrokenPut')) client._request_response = dict( status=200, data=self._encode_data(dummy_entries) ) client._put_err = [ ( gdata.service.RequestError, { 'status': '400', 'body': 'Sorry, there was an error saving the ' 'file. Please try again.', 'reason': 'Bad Request', }, ) ] client._listfeed = self._init_listfeed() self._init_keyring(client) new_pwd = 'newPwdBrokenPut' correct_read_entries = dict(sectionBrokenPut=dict(userBrokenPut=new_pwd)) client._request_response = dict( status=200, data=self._encode_data(correct_read_entries) ) self.keyring.set_password('sectionBrokenPut', 'userBrokenPut', new_pwd) self.assertEqual( self.keyring.get_password('sectionBrokenPut', 'userBrokenPut'), new_pwd ) def test_uses_existing_folder(self): import gdata client = self._init_client() # should not happen client._create_folder_err = gdata.service.RequestError self._init_keyring(client) self.assertEqual( self.keyring.get_password('service-a', 'user-A'), None, 'No password should be set in new keyring', ) client._listfeed = self._init_listfeed() self.keyring.set_password('service-a', 'user-A', 'password-A') self.assertIsNotNone(client._upload_data, 'Should have written data') self.assertEqual( self.keyring.get_password('service-a', 'user-A'), 'password-A', 'Correct password should be set', ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_Windows.py0000644000175100001730000000151714457340770017753 0ustar00runnerdockerfrom __future__ import print_function import sys import pytest from keyrings.alt import Windows from keyring.testing.backend import BackendBasicTests from .test_file import FileKeyringTests def is_win32_crypto_supported(): try: __import__('keyrings.alt._win_crypto') except ImportError: return False return sys.platform in ['win32'] and sys.getwindowsversion()[-2] == 2 @pytest.mark.skipif(not is_win32_crypto_supported(), reason="Need Windows") class TestWin32CryptoKeyring(FileKeyringTests): def init_keyring(self): return Windows.EncryptedKeyring() @pytest.mark.skipif( not Windows.RegistryKeyring.viable or sys.version_info < (3,), reason="RegistryKeyring not viable", ) class TestRegistryKeyring(BackendBasicTests): def init_keyring(self): return Windows.RegistryKeyring() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_crypto.py0000644000175100001730000000164014457340770017636 0ustar00runnerdockerimport getpass from unittest import mock import pytest from .test_file import FileKeyringTests from keyrings.alt import file def is_crypto_supported(): try: __import__('Cryptodome.Cipher.AES') __import__('Cryptodome.Protocol.KDF') __import__('Cryptodome.Random') except ImportError: try: __import__('Crypto.Cipher.AES') __import__('Crypto.Protocol.KDF') __import__('Crypto.Random') except ImportError: return False return True @pytest.mark.skipif(not is_crypto_supported(), reason="Need pycryptodomex module") class TestCryptedFileKeyring(FileKeyringTests): @pytest.fixture(autouse=True) def mocked_getpass(self, monkeypatch): fake_getpass = mock.Mock(return_value='abcdef') monkeypatch.setattr(getpass, 'getpass', fake_getpass) def init_keyring(self): return file.EncryptedKeyring() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_file.py0000644000175100001730000001721314457340770017240 0ustar00runnerdockerimport os import tempfile import sys import errno import getpass import configparser import pytest from unittest import mock from keyring.testing.backend import BackendBasicTests from keyring.testing.util import random_string from keyrings.alt import file from keyrings.alt.file_base import encodebytes from keyrings.alt.escape import escape as escape_for_ini from keyring.errors import PasswordDeleteError class FileKeyringTests(BackendBasicTests): @pytest.fixture(autouse=True) def _init_properties_for_file(self): self.keyring.file_path = tempfile.mktemp() yield @pytest.fixture(autouse=True) def _cleanup_for_file(self): yield try: os.remove(self.keyring.file_path) # remove file except OSError: # is a directory e = sys.exc_info()[1] if e.errno != errno.ENOENT: # No such file or directory raise def get_config(self): # setting a password triggers keyring file creation config = configparser.RawConfigParser() config.read(self.keyring.file_path) return config def save_config(self, config): with open(self.keyring.file_path, 'w') as config_file: config.write(config_file) def test_encrypt_decrypt(self): password = random_string(20) # keyring.encrypt expects bytes password = password.encode('utf-8') encrypted = self.keyring.encrypt(password) assert password == self.keyring.decrypt(encrypted) def test_encrypt_decrypt_without_assoc(self): # generate keyring self.set_password('system', 'user', 'password') config = self.get_config() # generate and save password without assoc data encrypted = self.keyring.encrypt('password'.encode('utf-8')) password_base64 = '\n' + encodebytes(encrypted).decode() config.set('system', 'user', password_base64) self.save_config(config) assert self.keyring.get_password('system', 'user') == 'password' def test_delete_password(self): self.set_password('system', 'user', 'password') with pytest.raises(PasswordDeleteError): self.keyring.delete_password('system', 'xxxx') with pytest.raises(PasswordDeleteError): self.keyring.delete_password('xxxxxx', 'xxxx') def test_file(self): if not hasattr(self.keyring, '_check_file'): return # keyring file doesn't exist yet assert self.keyring._check_file() is False # generate keyring self.set_password('system', 'user', 'password') # valid keyring file exist now assert self.keyring._check_file() is True # lock keyring self.keyring._lock() # fetch password from keyring assert self.keyring.get_password('system', 'user') == 'password' # test missing password reference config = self.get_config() krsetting = escape_for_ini('keyring-setting') pwref = escape_for_ini('password reference') # pwrefval = config.get(krsetting, pwref) config.remove_option(krsetting, pwref) self.save_config(config) assert self.keyring._check_file() is False def test_scheme(self): # scheme exists assert self.keyring.scheme is not None if not hasattr(self.keyring, '_check_file'): return # keyring file doesn't exist yet assert self.keyring._check_file() is False # generate keyring self.set_password('system', 'user', 'password') config = self.get_config() krsetting = escape_for_ini('keyring-setting') scheme = escape_for_ini('scheme') defscheme = '[PBKDF2] AES256.CFB' # default scheme match assert config.get(krsetting, scheme) == defscheme # invalid AES mode config.set(krsetting, scheme, defscheme.replace('CFB', 'XXX')) with pytest.raises(ValueError): self.keyring._check_scheme(config) # compatibility with former scheme format config.set(krsetting, scheme, 'PyCrypto ' + defscheme) assert self.keyring._check_scheme(config) is None # test with invalid KDF config.set(krsetting, scheme, defscheme.replace('PBKDF2', 'scrypt')) with pytest.raises(ValueError): self.keyring._check_scheme(config) # a missing scheme is valid config.remove_option(krsetting, scheme) self.save_config(config) assert self.keyring._check_file() is True with pytest.raises(AttributeError): self.keyring._check_scheme(config) def test_version(self): # version exists assert self.keyring.version is not None if not hasattr(self.keyring, '_check_version'): return # generate keyring self.set_password('system', 'user', 'password') config = self.get_config() # default version valid assert self.keyring._check_version(config) is True krsetting = escape_for_ini('keyring-setting') version = escape_for_ini('version') # invalid, if version is missing config.remove_option(krsetting, version) self.save_config(config) assert self.keyring._check_version(config) is False @pytest.fixture(scope="class") def monkeyclass(request): from _pytest.monkeypatch import MonkeyPatch mpatch = MonkeyPatch() yield mpatch mpatch.undo() @pytest.fixture(params=('Crypto', 'Cryptodome'), scope='class') def crypto_impl(request, monkeyclass): def unload_crypto(): matches = [mod for mod in sys.modules if mod.startswith('Crypto')] for mod in matches: del sys.modules[mod] keep = request.param suppress = 'Cryptodome' if keep == 'Crypto' else 'Crypto' monkeyclass.setitem(sys.modules, suppress, None) request.addfinalizer(unload_crypto) @pytest.mark.skipif( not file.EncryptedKeyring.viable, reason="EncryptedKeyring backend not viable", ) @pytest.mark.usefixtures('crypto_impl') class TestEncryptedFileKeyring(FileKeyringTests): @pytest.fixture(autouse=True) def crypt_fixture(self, monkeypatch): fake_getpass = mock.Mock(return_value='abcdef') monkeypatch.setattr(getpass, 'getpass', fake_getpass) def init_keyring(self): return file.EncryptedKeyring() def test_wrong_password(self): self.set_password('system', 'user', 'password') getpass.getpass.return_value = 'wrong' with pytest.raises(ValueError): self.keyring._unlock() @pytest.mark.skipif( sys.platform == 'win32', reason="Group/World permissions aren't meaningful on Windows", ) def test_keyring_not_created_world_writable(self): """ Ensure that when keyring creates the file that it's not overly- permissive. """ self.set_password('system', 'user', 'password') assert os.path.exists(self.keyring.file_path) group_other_perms = os.stat(self.keyring.file_path).st_mode & 0o077 assert group_other_perms == 0 class TestUncryptedFileKeyring(FileKeyringTests): def init_keyring(self): return file.PlaintextKeyring() @pytest.mark.skipif( sys.platform == 'win32', reason="Group/World permissions aren't meaningful on Windows", ) def test_keyring_not_created_world_writable(self): """ Ensure that when keyring creates the file that it's not overly- permissive. """ self.set_password('system', 'user', 'password') assert os.path.exists(self.keyring.file_path) group_other_perms = os.stat(self.keyring.file_path).st_mode & 0o077 assert group_other_perms == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_keyczar.py0000644000175100001730000000576614457340770020003 0ustar00runnerdockerimport os import unittest from keyrings.alt import keyczar from . import mocks class KeyczarCrypterTestCase(unittest.TestCase): """Test the keyczar crypter""" def setUp(self): self._orig_keyczar = keyczar.keyczar if hasattr(keyczar, 'keyczar') else None keyczar.keyczar = mocks.MockKeyczar() def tearDown(self): keyczar.keyczar = self._orig_keyczar if keyczar.EnvironCrypter.KEYSET_ENV_VAR in os.environ: del os.environ[keyczar.EnvironCrypter.KEYSET_ENV_VAR] if keyczar.EnvironCrypter.ENC_KEYSET_ENV_VAR in os.environ: del os.environ[keyczar.EnvironCrypter.ENC_KEYSET_ENV_VAR] def testKeyczarCrypterWithUnencryptedReader(self): """ """ location = 'bar://baz' kz_crypter = keyczar.Crypter(location) self.assertEqual(location, kz_crypter.keyset_location) self.assertIsNone(kz_crypter.encrypting_keyset_location) self.assertIsInstance(kz_crypter.crypter, mocks.MockKeyczarCrypter) self.assertIsInstance(kz_crypter.crypter.reader, mocks.MockKeyczarReader) self.assertEqual(location, kz_crypter.crypter.reader.location) def testKeyczarCrypterWithEncryptedReader(self): """ """ location = 'foo://baz' encrypting_location = 'castle://aaargh' kz_crypter = keyczar.Crypter(location, encrypting_location) self.assertEqual(location, kz_crypter.keyset_location) self.assertEqual(encrypting_location, kz_crypter.encrypting_keyset_location) self.assertIsInstance(kz_crypter.crypter, mocks.MockKeyczarCrypter) self.assertIsInstance( kz_crypter.crypter.reader, mocks.MockKeyczarEncryptedReader ) self.assertEqual(location, kz_crypter.crypter.reader._reader.location) self.assertEqual( encrypting_location, kz_crypter.crypter.reader._crypter.reader.location ) def testKeyczarCrypterEncryptDecryptHandlesEmptyNone(self): location = 'castle://aargh' kz_crypter = keyczar.Crypter(location) self.assertEqual('', kz_crypter.encrypt('')) self.assertEqual('', kz_crypter.encrypt(None)) self.assertEqual('', kz_crypter.decrypt('')) self.assertEqual('', kz_crypter.decrypt(None)) def testEnvironCrypterReadsCorrectValues(self): location = 'foo://baz' encrypting_location = 'castle://aaargh' kz_crypter = keyczar.EnvironCrypter() os.environ[kz_crypter.KEYSET_ENV_VAR] = location self.assertEqual(location, kz_crypter.keyset_location) self.assertIsNone(kz_crypter.encrypting_keyset_location) os.environ[kz_crypter.ENC_KEYSET_ENV_VAR] = encrypting_location self.assertEqual(encrypting_location, kz_crypter.encrypting_keyset_location) def testEnvironCrypterThrowsExceptionOnMissingValues(self): kz_crypter = keyczar.EnvironCrypter() with self.assertRaises(ValueError): kz_crypter.keyset_location self.assertIsNone(kz_crypter.encrypting_keyset_location) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tests/test_multi.py0000644000175100001730000000403414457340770017450 0ustar00runnerdockerimport unittest from keyring.backend import KeyringBackend from keyrings.alt import multi import keyring.errors class MultipartKeyringWrapperTestCase(unittest.TestCase): """Test the wrapper that breaks passwords into smaller chunks""" class MockKeyring(KeyringBackend): priority = 1 # type: ignore def __init__(self): self.passwords = {} def get_password(self, service, username): return self.passwords.get(service + username) def set_password(self, service, username, password): self.passwords[service + username] = password def delete_password(self, service, username): try: del self.passwords[service + username] except KeyError: raise keyring.errors.PasswordDeleteError('not found') def testViablePassThru(self): kr = multi.MultipartKeyringWrapper(self.MockKeyring()) self.assertTrue(kr.viable) def testMissingPassword(self): wrapped_kr = self.MockKeyring() kr = multi.MultipartKeyringWrapper(wrapped_kr) self.assertIsNone(kr.get_password('s1', 'u1')) def testSmallPasswordSetInSinglePart(self): wrapped_kr = self.MockKeyring() kr = multi.MultipartKeyringWrapper(wrapped_kr) kr.set_password('s1', 'u1', 'p1') self.assertEqual(wrapped_kr.passwords, {'s1u1': 'p1'}) # should be able to read it back self.assertEqual(kr.get_password('s1', 'u1'), 'p1') def testLargePasswordSetInMultipleParts(self): wrapped_kr = self.MockKeyring() kr = multi.MultipartKeyringWrapper(wrapped_kr, max_password_size=2) kr.set_password('s2', 'u2', '0123456') self.assertEqual( wrapped_kr.passwords, { 's2u2': '01', 's2u2{{part_1}}': '23', 's2u2{{part_2}}': '45', "s2u2{{part_3}}": '6', }, ) # should be able to read it back self.assertEqual(kr.get_password('s2', 'u2'), '0123456') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/towncrier.toml0000644000175100001730000000005414457340770016452 0ustar00runnerdocker[tool.towncrier] title_format = "{version}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1690157560.0 keyrings.alt-5.0.0/tox.ini0000644000175100001730000000140314457340770015053 0ustar00runnerdocker[testenv] deps = setenv = PYTHONWARNDEFAULTENCODING = 1 commands = pytest {posargs} usedevelop = True extras = testing [testenv:docs] extras = docs testing changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html python -m sphinxlint [testenv:finalize] skip_install = True deps = towncrier jaraco.develop >= 7.23 passenv = * commands = python -m jaraco.develop.finalize [testenv:release] skip_install = True deps = build twine>=3 jaraco.develop>=7.1 passenv = TWINE_PASSWORD GITHUB_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release