././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.8033884 keyrings.alt-3.4.0/0000775000372000037200000000000000000000000014773 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/.coveragerc0000664000372000037200000000115600000000000017117 0ustar00travistravis00000000000000[run] omit = .tox/* branch = True [report] exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: # don't try to cover abstracts @abc.abstractmethod @abc.abstractproperty # don't try to cover special properties @properties.NonDataProperty show_missing = True ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/.flake80000664000372000037200000000036600000000000016153 0ustar00travistravis00000000000000[flake8] max-line-length = 88 ignore = # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 W504 # Black creates whitespace before colon E203 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/.pre-commit-config.yaml0000664000372000037200000000012100000000000021246 0ustar00travistravis00000000000000repos: - repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/.readthedocs.yml0000664000372000037200000000011200000000000020053 0ustar00travistravis00000000000000python: version: 3 extra_requirements: - docs pip_install: true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/.travis.yml0000664000372000037200000000072200000000000017105 0ustar00travistravis00000000000000dist: xenial language: python python: - 3.6 - &latest_py3 3.8 jobs: fast_finish: true include: - stage: deploy if: tag IS present python: *latest_py3 before_script: skip script: tox -e release cache: pip install: - pip install tox tox-venv before_script: # Disable IPv6. Ref travis-ci/travis-ci#8361 - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: tox ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/CHANGES.rst0000664000372000037200000000326400000000000016602 0ustar00travistravis00000000000000v3.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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/LICENSE0000664000372000037200000000203200000000000015775 0ustar00travistravis00000000000000Copyright Jason R. Coombs Permission 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.8033884 keyrings.alt-3.4.0/PKG-INFO0000664000372000037200000000407300000000000016074 0ustar00travistravis00000000000000Metadata-Version: 2.1 Name: keyrings.alt Version: 3.4.0 Summary: Alternate keyring implementations Home-page: https://github.com/jaraco/keyrings.alt Author: Jason R. Coombs Author-email: jaraco@jaraco.com License: UNKNOWN Description: .. 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://img.shields.io/travis/jaraco/keyrings.alt/master.svg :target: https://travis-ci.org/jaraco/keyrings.alt .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://img.shields.io/appveyor/ci/jaraco/keyrings-alt/master.svg :target: https://ci.appveyor.com/project/jaraco/keyrings-alt/branch/master .. .. image:: https://readthedocs.org/projects/keyringsalt/badge/?version=latest .. :target: https://keyringsalt.readthedocs.io/en/latest/?badge=latest 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. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.6 Provides-Extra: testing Provides-Extra: docs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/README.rst0000664000372000037200000000250200000000000016461 0ustar00travistravis00000000000000.. 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://img.shields.io/travis/jaraco/keyrings.alt/master.svg :target: https://travis-ci.org/jaraco/keyrings.alt .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://img.shields.io/appveyor/ci/jaraco/keyrings-alt/master.svg :target: https://ci.appveyor.com/project/jaraco/keyrings-alt/branch/master .. .. image:: https://readthedocs.org/projects/keyringsalt/badge/?version=latest .. :target: https://keyringsalt.readthedocs.io/en/latest/?badge=latest 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/appveyor.yml0000664000372000037200000000072400000000000017366 0ustar00travistravis00000000000000environment: APPVEYOR: true matrix: - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" install: # symlink python from a directory with a space - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" - "SET PYTHON=\"C:\\Program Files\\Python\"" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" build: off cache: - '%LOCALAPPDATA%\pip\Cache' test_script: - "python -m pip install -U tox tox-venv virtualenv" - "tox" version: '{build}' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/conftest.py0000664000372000037200000000020200000000000017164 0ustar00travistravis00000000000000import platform collect_ignore = [] if platform.system() != 'Windows': collect_ignore.append('keyrings/alt/_win_crypto.py') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.7993865 keyrings.alt-3.4.0/docs/0000775000372000037200000000000000000000000015723 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/docs/conf.py0000664000372000037200000000136300000000000017225 0ustar00travistravis00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] master_doc = "index" link_files = { '../CHANGES.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://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), ], ) } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/docs/history.rst0000664000372000037200000000012100000000000020150 0ustar00travistravis00000000000000:tocdepth: 2 .. _changes: History ******* .. include:: ../CHANGES (links).rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/docs/index.rst0000664000372000037200000000154400000000000017570 0ustar00travistravis00000000000000Welcome to keyrings.alt documentation! ====================================== .. toctree:: :maxdepth: 1 history .. 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.pyfs :members: :undoc-members: :show-inheritance: .. automodule:: keyrings.alt.Windows :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.7993865 keyrings.alt-3.4.0/keyrings/0000775000372000037200000000000000000000000016626 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/__init__.py0000664000372000037200000000010100000000000020727 0ustar00travistravis00000000000000__path__ = __import__('pkgutil').extend_path(__path__, __name__) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.8033884 keyrings.alt-3.4.0/keyrings/alt/0000775000372000037200000000000000000000000017406 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/Gnome.py0000664000372000037200000001050700000000000021030 0ustar00travistravis00000000000000try: import gi gi.require_version('GnomeKeyring', '1.0') from gi.repository import GnomeKeyring except (ImportError, ValueError, AttributeError): pass from keyring.backend import KeyringBackend from keyring.errors import PasswordSetError, PasswordDeleteError from keyring.util import properties 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 @classmethod 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/Google.py0000664000372000037200000003026300000000000021200 0ustar00travistravis00000000000000from __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 . import keyczar from keyring import errors from keyring import credentials from keyring.backend import KeyringBackend from keyring.util import properties 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 @classmethod 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/Windows.py0000664000372000037200000001242000000000000021411 0ustar00travistravis00000000000000import sys import base64 import platform import functools from keyring.util 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 @classmethod 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 @classmethod 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.decodestring(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.encodestring(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 class OldPywinError(object): """ A compatibility wrapper for old PyWin32 errors, such as reported in https://bitbucket.org/kang/python-keyring-lib/issue/140/ """ def __init__(self, orig): self.orig = orig @property def funcname(self): return self.orig[1] @property def winerror(self): return self.orig[0] @classmethod def wrap(cls, orig_err): attr_check = functools.partial(hasattr, orig_err) is_old = not all(map(attr_check, ['funcname', 'winerror'])) return cls(orig_err) if is_old else orig_err ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/__init__.py0000664000372000037200000000000000000000000021505 0ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/_win_crypto.py0000664000372000037200000000553000000000000022317 0ustar00travistravis00000000000000from __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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/escape.py0000664000372000037200000000220100000000000021213 0ustar00travistravis00000000000000""" 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/file.py0000664000372000037200000001636700000000000020714 0ustar00travistravis00000000000000from __future__ import with_statement import os import sys import json import getpass import configparser from keyring.util 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 "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(object): """ PyCrypto-backed Encryption support """ scheme = '[PBKDF2] AES256.CFB' version = '1.0' block_size = 32 def _create_cipher(self, password, salt, IV): """ Create the cipher object to encrypt or decrypt a payload. """ from Crypto.Protocol.KDF import PBKDF2 from Crypto.Cipher import AES pw = PBKDF2(password, salt, dkLen=self.block_size) return AES.new(pw[: self.block_size], 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): """PyCrypto File Keyring""" filename = 'crypted_pass.cfg' pw_prefix = 'pw:'.encode() @properties.ClassProperty @classmethod def priority(self): "Applicable for all platforms, but not recommended." try: __import__('Crypto.Cipher.AES') __import__('Crypto.Protocol.KDF') __import__('Crypto.Random') except ImportError: # pragma: no cover raise RuntimeError("PyCrypto 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 from Crypto.Random import get_random_bytes salt = get_random_bytes(self.block_size) from Crypto.Cipher import AES IV = get_random_bytes(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. """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/file_base.py0000664000372000037200000001476500000000000021706 0ustar00travistravis00000000000000from __future__ import with_statement import os import abc import base64 import configparser from keyring.errors import PasswordDeleteError from keyring.backend import KeyringBackend from keyring.util import platform_, properties from .escape import escape as escape_for_ini try: encodebytes = base64.encodebytes except AttributeError: # pragma: no cover encodebytes = base64.encodestring try: decodebytes = base64.decodebytes except AttributeError: # pragma: no cover decodebytes = base64.decodestring class FileBacked(object): @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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/keyczar.py0000664000372000037200000000550400000000000021434 0ustar00travistravis00000000000000from __future__ import absolute_import import os import abc try: from keyczar import keyczar except ImportError: pass from keyring.backend import Crypter from keyring import errors def has_keyczar(): with errors.ExceptionRaisedContext() as exc: keyczar.__name__ return not bool(exc) class BaseCrypter(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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/multi.py0000664000372000037200000000421200000000000021111 0ustar00travistravis00000000000000import itertools from keyring.util 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 @classmethod 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/pyfs.py0000664000372000037200000002323100000000000020742 0ustar00travistravis00000000000000import os import base64 import sys import configparser from keyring import errors from .escape import escape as escape_for_ini from keyring.util import platform_, properties from keyring.backend import KeyringBackend, NullCrypter from . import keyczar try: import fs.opener import fs.osfs import fs.errors import fs.path import fs.remote except ImportError: pass def has_pyfs(): """ Does this environment have pyfs 1.x installed? Should return False even when Mercurial's Demand Import allowed import of fs.*. """ with errors.ExceptionRaisedContext() as exc: fs.opener.opener return not bool(exc) class BasicKeyring(KeyringBackend): """BasicKeyring is a Pyfilesystem-based implementation of keyring. It stores the password directly in the file, and supports encryption and decryption. The encrypted password is stored in base64 format. Being based on Pyfilesystem the file can be local or network-based and served by any of the filesystems supported by Pyfilesystem including Amazon S3, FTP, WebDAV, memory and more. """ _filename = 'keyring_pyf_pass.cfg' def __init__(self, crypter, filename=None, can_create=True, cache_timeout=None): super(BasicKeyring, self).__init__() self._crypter = crypter def_fn = os.path.join(platform_.data_root(), self.__class__._filename) self._filename = filename or def_fn self._can_create = can_create self._cache_timeout = cache_timeout @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) @property def filename(self): """The filename used to store the passwords. """ return self._filename def encrypt(self, password): """Encrypt the password. """ if not password or not self._crypter: return password or b'' return self._crypter.encrypt(password) def decrypt(self, password_encrypted): """Decrypt the password. """ if not password_encrypted or not self._crypter: return password_encrypted or b'' return self._crypter.decrypt(password_encrypted) def _open(self, mode='r'): """Open the password file in the specified mode """ open_file = None writeable = 'w' in mode or 'a' in mode or '+' in mode try: # NOTE: currently the MemOpener does not split off any filename # which causes errors on close() # so we add a dummy name and open it separately if self.filename.startswith('mem://') or self.filename.startswith('ram://'): open_file = fs.opener.fsopendir(self.filename).open('kr.cfg', mode) else: if not hasattr(self, '_pyfs'): # reuse the pyfilesystem and path self._pyfs, self._path = fs.opener.opener.parse( self.filename, writeable=writeable ) # cache if permitted if self._cache_timeout is not None: self._pyfs = fs.remote.CacheFS( self._pyfs, cache_timeout=self._cache_timeout ) open_file = self._pyfs.open(self._path, mode) except fs.errors.ResourceNotFoundError: if self._can_create: segments = fs.opener.opener.split_segments(self.filename) if segments: # this seems broken, but pyfilesystem uses it, so we must fs_name, credentials, url1, url2, path = segments.groups() assert fs_name, 'Should be a remote filesystem' host = '' # allow for domain:port if ':' in url2: split_url2 = url2.split('/', 1) if len(split_url2) > 1: url2 = split_url2[1] else: url2 = '' host = split_url2[0] pyfs = fs.opener.opener.opendir('%s://%s' % (fs_name, host)) # cache if permitted if self._cache_timeout is not None: pyfs = fs.remote.CacheFS( pyfs, cache_timeout=self._cache_timeout ) # NOTE: fs.path.split does not function in the same # way os os.path.split... at least under windows url2_path, url2_filename = os.path.split(url2) if url2_path and not pyfs.exists(url2_path): pyfs.makedir(url2_path, recursive=True) else: # assume local filesystem full_url = fs.opener._expand_syspath(self.filename) # NOTE: fs.path.split does not function in the same # way os os.path.split... at least under windows url2_path, url2 = os.path.split(full_url) pyfs = fs.osfs.OSFS(url2_path) try: # reuse the pyfilesystem and path self._pyfs = pyfs self._path = url2 return pyfs.open(url2, mode) except fs.errors.ResourceNotFoundError: if writeable: raise else: pass # NOTE: ignore read errors as the underlying caller can fail safely if writeable: raise else: pass return open_file @property def config(self): """load the passwords from the config file """ if not hasattr(self, '_config'): raw_config = configparser.RawConfigParser() f = self._open() if f: raw_config.readfp(f) f.close() self._config = raw_config return self._config def get_password(self, service, username): """Read the password from the file. """ service = escape_for_ini(service) username = escape_for_ini(username) # fetch the password try: password_base64 = self.config.get(service, username).encode() # decode with base64 password_encrypted = base64.decodestring(password_base64) # decrypted the password 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. """ service = escape_for_ini(service) username = escape_for_ini(username) # encrypt the password password = password or '' password_encrypted = self.encrypt(password.encode('utf-8')) # encode with base64 password_base64 = base64.encodestring(password_encrypted).decode() # write the modification if not self.config.has_section(service): self.config.add_section(service) self.config.set(service, username, password_base64) config_file = UnicodeWriterAdapter(self._open('w')) self.config.write(config_file) config_file.close() def delete_password(self, service, username): service = escape_for_ini(service) username = escape_for_ini(username) try: self.config.remove_option(service, username) except configparser.NoSectionError: raise errors.PasswordDeleteError('Password not found') config_file = UnicodeWriterAdapter(self._open('w')) self.config.write(config_file) config_file.close() @properties.ClassProperty @classmethod def priority(cls): if not has_pyfs(): raise RuntimeError("pyfs required") return 2 class UnicodeWriterAdapter(object): """ Wrap an object with a .write method to accept 'str' on Python 2 and make it a Unicode string. """ def __init__(self, orig): self._orig = orig def __getattr__(self, *args, **kwargs): return getattr(self._orig, *args, **kwargs) def write(self, value): if isinstance(value, str): value = value.decode('ascii') return self._orig.write(value) if sys.version_info > (3,): def UnicodeWriterAdapter(x): # noqa return x class PlaintextKeyring(BasicKeyring): """Unencrypted Pyfilesystem Keyring """ def __init__(self, filename=None, can_create=True, cache_timeout=None): super(PlaintextKeyring, self).__init__( NullCrypter(), filename=filename, can_create=can_create, cache_timeout=cache_timeout, ) class EncryptedKeyring(BasicKeyring): """Encrypted Pyfilesystem Keyring """ _filename = 'crypted_pyf_pass.cfg' def __init__(self, crypter, filename=None, can_create=True, cache_timeout=None): super(EncryptedKeyring, self).__init__( crypter, filename=filename, can_create=can_create, cache_timeout=cache_timeout, ) class KeyczarKeyring(EncryptedKeyring): """Encrypted Pyfilesystem Keyring using Keyczar keysets specified in environment vars """ def __init__(self): super(KeyczarKeyring, self).__init__(keyczar.EnvironCrypter()) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.8033884 keyrings.alt-3.4.0/keyrings/alt/tests/0000775000372000037200000000000000000000000020550 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/__init__.py0000664000372000037200000000000000000000000022647 0ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/mocks.py0000664000372000037200000001455300000000000022246 0ustar00travistravis00000000000000""" Various mock objects for testing """ import base64 import io import pickle class MockAtom(object): """ Mocks an atom in the GData service. """ def __init__(self, value): self.text = value class MockEntry(object): """ 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(object): """ Mocks the functionality of an http client. """ def request(*args, **kwargs): pass class MockGDataService(object): """ 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, object): 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(object): @property def entry(self): if hasattr(self, '_entry'): return self._entry return [] class MockListEntry(object): pass class MockLink(object): @property def href(self): return '' class MockContent(object): @property def src(self): return 'src' class MockDocumentListEntry(object): @property def content(self): return MockContent() def GetEditMediaLink(self): return MockLink() class MockKeyczarReader(object): def __init__(self, location): self.location = location class MockKeyczarEncryptedReader(object): def __init__(self, reader, crypter): self._reader = reader self._crypter = crypter class MockKeyczarReaders(object): @staticmethod def CreateReader(location): return MockKeyczarReader(location) @staticmethod def EncryptedReader(reader, crypter): return MockKeyczarEncryptedReader(reader, crypter) class MockKeyczarCrypter(object): def __init__(self, reader): self.reader = reader @staticmethod def Read(location): return MockKeyczarCrypter(MockKeyczarReader(location)) class MockKeyczar(object): @property def readers(self): return MockKeyczarReaders @property def Crypter(self): return MockKeyczarCrypter ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_Gnome.py0000664000372000037200000000212300000000000023224 0ustar00travistravis00000000000000import types import sys import unittest from keyring.tests.test_backend import BackendBasicTests from keyring.tests.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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_Google.py0000664000372000037200000003567200000000000023412 0ustar00travistravis00000000000000import codecs import base64 import unittest import pickle from keyring.tests.test_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', ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_Windows.py0000664000372000037200000000151400000000000023614 0ustar00travistravis00000000000000from __future__ import print_function import sys import pytest from keyrings.alt import Windows from keyring.tests.test_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(), "Need Windows") class Win32CryptoKeyringTestCase(FileKeyringTests): def init_keyring(self): return Windows.EncryptedKeyring() @pytest.mark.skipif( not Windows.RegistryKeyring.viable or sys.version_info < (3,), "RegistryKeyring not viable", ) class RegistryKeyringTestCase(BackendBasicTests): def init_keyring(self): return Windows.RegistryKeyring() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_crypto.py0000664000372000037200000000135000000000000023500 0ustar00travistravis00000000000000import getpass import unittest from unittest import mock import pytest from .test_file import FileKeyringTests from keyrings.alt import file def is_crypto_supported(): try: __import__('Crypto.Cipher.AES') __import__('Crypto.Protocol.KDF') __import__('Crypto.Random') except ImportError: return False return True @unittest.skipUnless(is_crypto_supported(), "Need Crypto module") class CryptedFileKeyringTestCase(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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_file.py0000664000372000037200000001610200000000000023100 0ustar00travistravis00000000000000import os import tempfile import sys import errno import unittest import getpass import configparser import pytest from unittest import mock from keyring.tests.test_backend import BackendBasicTests from keyring.tests.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 keyring_with_file(self): self.keyring.file_path = self.tmp_keyring_file = tempfile.mktemp() yield try: os.unlink(self.tmp_keyring_file) except (OSError,): 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) self.assertEqual(password, self.keyring.decrypt(encrypted)) def test_encrypt_decrypt_without_assoc(self): # generate keyring self.keyring.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) self.assertEqual(self.keyring.get_password('system', 'user'), 'password') def test_delete_password(self): self.keyring.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 self.assertTrue(self.keyring._check_file() is False) # generate keyring self.keyring.set_password('system', 'user', 'password') # valid keyring file exist now self.assertTrue(self.keyring._check_file() is True) # lock keyring self.keyring._lock() # fetch password from keyring self.assertTrue(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) self.assertTrue(self.keyring._check_file() is False) def test_scheme(self): # scheme exists self.assertTrue(self.keyring.scheme is not None) if not hasattr(self.keyring, '_check_file'): return # keyring file doesn't exist yet self.assertTrue(self.keyring._check_file() is False) # generate keyring self.keyring.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 self.assertTrue(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) self.assertTrue(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) self.assertTrue(self.keyring._check_file() is True) with pytest.raises(AttributeError): self.keyring._check_scheme(config) def test_version(self): # version exists self.assertTrue(self.keyring.version is not None) if not hasattr(self.keyring, '_check_version'): return # generate keyring self.keyring.set_password('system', 'user', 'password') config = self.get_config() # default version valid self.assertTrue(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) self.assertTrue(self.keyring._check_version(config) is False) class EncryptedFileKeyringTestCase(FileKeyringTests): @pytest.fixture(autouse=True, scope='class') def crypt_fixture(self, monkeypatch): pytest.importorskip('Crypto') 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.keyring.set_password('system', 'user', 'password') getpass.getpass.return_value = 'wrong' with pytest.raises(ValueError): self.keyring._unlock() @unittest.skipIf( sys.platform == 'win32', "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.keyring.set_password('system', 'user', 'password') self.assertTrue(os.path.exists(self.keyring.file_path)) group_other_perms = os.stat(self.keyring.file_path).st_mode & 0o077 self.assertEqual(group_other_perms, 0) class UncryptedFileKeyringTestCase(FileKeyringTests): def init_keyring(self): return file.PlaintextKeyring() @unittest.skipIf( sys.platform == 'win32', "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.keyring.set_password('system', 'user', 'password') self.assertTrue(os.path.exists(self.keyring.file_path)) group_other_perms = os.stat(self.keyring.file_path).st_mode & 0o077 self.assertEqual(group_other_perms, 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_keyczar.py0000664000372000037200000000600600000000000023633 0ustar00travistravis00000000000000import 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_multi.py0000664000372000037200000000401500000000000023313 0ustar00travistravis00000000000000import 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 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/keyrings/alt/tests/test_pyfs.py0000664000372000037200000000775600000000000023161 0ustar00travistravis00000000000000from __future__ import unicode_literals import os import tempfile import textwrap import unittest import pytest import keyring.backend from keyrings.alt import pyfs from keyring.tests.test_backend import BackendBasicTests, random_string class ReverseCrypter(keyring.backend.Crypter): """Very silly crypter class""" def encrypt(self, value): return value[::-1] def decrypt(self, value): return value[::-1] class PyfilesystemKeyringTests(BackendBasicTests): """Base class for Pyfilesystem tests""" def test_encrypt_decrypt(self): password = random_string(20) encrypted = self.keyring.encrypt(password) self.assertEqual(password, self.keyring.decrypt(encrypted)) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class UnencryptedMemoryPyfilesystemKeyringNoSubDirTestCase(PyfilesystemKeyringTests): """Test in memory with no encryption""" keyring_filename = 'mem://unencrypted' def init_keyring(self): return pyfs.PlaintextKeyring(filename=self.keyring_filename) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class UnencryptedMemoryPyfilesystemKeyringSubDirTestCase(PyfilesystemKeyringTests): """Test in memory with no encryption""" keyring_filename = 'mem://some/sub/dir/unencrypted' def init_keyring(self): return pyfs.PlaintextKeyring(filename=self.keyring_filename) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class UnencryptedLocalPyfilesystemKeyringNoSubDirTestCase(PyfilesystemKeyringTests): """Test using local temp files with no encryption""" keyring_filename = '%s/keyring.cfg' % tempfile.mkdtemp() def init_keyring(self): return pyfs.PlaintextKeyring(filename=self.keyring_filename) def test_handles_preexisting_keyring(self): from fs.opener import opener fs, path = opener.parse(self.keyring_filename, writeable=True) keyring_file = fs.open(path, 'w') file_data = textwrap.dedent( """ [svc1] user1 = cHdkMQ== """ ).lstrip() keyring_file.write(file_data) keyring_file.close() pyf_keyring = pyfs.PlaintextKeyring(filename=self.keyring_filename) self.assertEqual('pwd1', pyf_keyring.get_password('svc1', 'user1')) @pytest.fixture(autouse=True) def remove_keyring_filename(self): if os.path.exists(self.keyring_filename): os.remove(self.keyring_filename) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class UnencryptedLocalPyfilesystemKeyringSubDirTestCase(PyfilesystemKeyringTests): """Test using local temp files with no encryption""" keyring_dir = os.path.join(tempfile.mkdtemp(), 'more', 'sub', 'dirs') keyring_filename = os.path.join(keyring_dir, 'keyring.cfg') def init_keyring(self): if not os.path.exists(self.keyring_dir): os.makedirs(self.keyring_dir) return pyfs.PlaintextKeyring(filename=self.keyring_filename) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class EncryptedMemoryPyfilesystemKeyringTestCase(PyfilesystemKeyringTests): """Test in memory with encryption""" def init_keyring(self): return pyfs.EncryptedKeyring( ReverseCrypter(), filename='mem://encrypted/keyring.cfg' ) @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class EncryptedLocalPyfilesystemKeyringNoSubDirTestCase(PyfilesystemKeyringTests): """Test using local temp files with encryption""" def init_keyring(self): return pyfs.EncryptedKeyring(ReverseCrypter(), filename='temp://keyring.cfg') @unittest.skipUnless(pyfs.BasicKeyring.viable, "Need Pyfilesystem") class EncryptedLocalPyfilesystemKeyringSubDirTestCase(PyfilesystemKeyringTests): """Test using local temp files with encryption""" def init_keyring(self): return pyfs.EncryptedKeyring( ReverseCrypter(), filename='temp://a/sub/dir/hierarchy/keyring.cfg' ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.7993865 keyrings.alt-3.4.0/keyrings.alt.egg-info/0000775000372000037200000000000000000000000021077 5ustar00travistravis00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/PKG-INFO0000664000372000037200000000407300000000000022200 0ustar00travistravis00000000000000Metadata-Version: 2.1 Name: keyrings.alt Version: 3.4.0 Summary: Alternate keyring implementations Home-page: https://github.com/jaraco/keyrings.alt Author: Jason R. Coombs Author-email: jaraco@jaraco.com License: UNKNOWN Description: .. 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://img.shields.io/travis/jaraco/keyrings.alt/master.svg :target: https://travis-ci.org/jaraco/keyrings.alt .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://img.shields.io/appveyor/ci/jaraco/keyrings-alt/master.svg :target: https://ci.appveyor.com/project/jaraco/keyrings-alt/branch/master .. .. image:: https://readthedocs.org/projects/keyringsalt/badge/?version=latest .. :target: https://keyringsalt.readthedocs.io/en/latest/?badge=latest 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. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.6 Provides-Extra: testing Provides-Extra: docs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/SOURCES.txt0000664000372000037200000000204700000000000022766 0ustar00travistravis00000000000000.coveragerc .flake8 .pre-commit-config.yaml .readthedocs.yml .travis.yml CHANGES.rst LICENSE README.rst appveyor.yml conftest.py pyproject.toml pytest.ini setup.cfg setup.py skeleton.md tox.ini 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 keyrings/alt/pyfs.py keyrings/alt/tests/__init__.py keyrings/alt/tests/mocks.py keyrings/alt/tests/test_Gnome.py keyrings/alt/tests/test_Google.py keyrings/alt/tests/test_Windows.py keyrings/alt/tests/test_crypto.py keyrings/alt/tests/test_file.py keyrings/alt/tests/test_keyczar.py keyrings/alt/tests/test_multi.py keyrings/alt/tests/test_pyfs.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/dependency_links.txt0000664000372000037200000000000100000000000025145 0ustar00travistravis00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/entry_points.txt0000664000372000037200000000033500000000000024376 0ustar00travistravis00000000000000[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 pyfs = keyrings.alt.pyfs ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/requires.txt0000664000372000037200000000042600000000000023501 0ustar00travistravis00000000000000 [docs] sphinx jaraco.packaging>=3.2 rst.linker>=1.9 [testing] pytest!=3.7.3,>=3.5 pytest-checkdocs>=1.2.3 pytest-flake8 pytest-black-multipy pytest-cov backports.unittest_mock keyring<20,>=10.3.1 fs<2,>=0.5 pycryptodome [testing:python_version == "2.7"] gdata python-keyczar ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818535.0 keyrings.alt-3.4.0/keyrings.alt.egg-info/top_level.txt0000664000372000037200000000001100000000000023621 0ustar00travistravis00000000000000keyrings ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/pyproject.toml0000664000372000037200000000024700000000000017712 0ustar00travistravis00000000000000[build-system] requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/pytest.ini0000664000372000037200000000023100000000000017020 0ustar00travistravis00000000000000[pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1575818535.8073905 keyrings.alt-3.4.0/setup.cfg0000664000372000037200000000236100000000000016616 0ustar00travistravis00000000000000[bdist_wheel] universal = 1 [metadata] license_file = LICENSE 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 [options] packages = find: include_package_data = true python_requires = >=3.6 install_requires = setup_requires = setuptools_scm >= 1.15.0 [options.packages.find] exclude = tests [options.extras_require] testing = pytest >= 3.5, !=3.7.3 pytest-checkdocs >= 1.2.3 pytest-flake8 pytest-black-multipy pytest-cov backports.unittest_mock keyring >= 10.3.1, < 20 fs>=0.5,<2 pycryptodome gdata; python_version=="2.7" python-keyczar; python_version=="2.7" docs = sphinx jaraco.packaging >= 3.2 rst.linker >= 1.9 [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 pyfs = keyrings.alt.pyfs Windows (alt) = keyrings.alt.Windows [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/setup.py0000664000372000037200000000016000000000000016502 0ustar00travistravis00000000000000#!/usr/bin/env python import setuptools if __name__ == "__main__": setuptools.setup(use_scm_version=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/skeleton.md0000664000372000037200000001771200000000000017151 0ustar00travistravis00000000000000# Overview This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. ## An SCM Managed Approach While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices. It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. # Usage ## new projects To use skeleton for a new project, simply pull the skeleton into a new project: ``` $ git init my-new-project $ cd my-new-project $ git pull gh://jaraco/skeleton ``` Now customize the project to suit your individual project needs. ## existing projects If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. ``` $ git merge skeleton --allow-unrelated-histories ``` The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. ## Updating Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations. Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. # Features The features/techniques employed by the skeleton include: - PEP 517/518 based build relying on setuptools as the build tool - setuptools declarative configuration using setup.cfg - tox for running tests - A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out - A CHANGES.rst file intended for publishing release notes about the project - Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) ## Packaging Conventions A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config). The setup.cfg file implements the following features: - Assumes universal wheel for release - Advertises the project's LICENSE file (MIT by default) - Reads the README.rst file into the long description - Some common Trove classifiers - Includes all packages discovered in the repo - Data files in the package are also included (not just Python files) - Declares the required Python versions - Declares install requirements (empty by default) - Declares setup requirements for legacy environments - Supplies two 'extras': - testing: requirements for running tests - docs: requirements for building docs - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts - Placeholder for defining entry points Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: - derive the project version from SCM tags - ensure that all files committed to the repo are automatically included in releases ## Running Tests The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). Other environments (invoked with `tox -e {name}`) supplied include: - a `docs` environment to build the documentation - a `release` environment to publish the package to PyPI A pytest.ini is included to define common options around running tests. In particular: - rely on default test discovery in the current directory - avoid recursing into common directories not containing tests - run doctests on modules and invoke flake8 tests - in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. - filters out known warnings caused by libraries/functionality included by the skeleton Relies a .flake8 file to correct some default behaviors: - disable mutually incompatible rules W503 and W504 - support for black format ## Continuous Integration The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. Features include: - test against Python 2 and 3 - run on Ubuntu Xenial - correct for broken IPv6 Also provided is a minimal template for running under Appveyor (Windows). ### Continuous Deployments In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax): ``` TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD ``` ## Building Documentation Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. ## Cutting releases By default, tagged commits are released through the continuous integration deploy stage. Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: ``` TWINE_PASSWORD={token} tox -e release ``` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1575818509.0 keyrings.alt-3.4.0/tox.ini0000664000372000037200000000137100000000000016310 0ustar00travistravis00000000000000[tox] envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true # Ensure that a late version of pip is used even on tox-venv. requires = tox-pip-version>=0.0.6 tox-venv [testenv] deps = setuptools>=31.0.1 pip_version = pip commands = pytest {posargs} usedevelop = True extras = testing [testenv:docs] extras = docs testing changedir = docs commands = python -m sphinx . {toxinidir}/build/html [testenv:release] skip_install = True deps = pep517>=0.5 twine[keyring]>=1.13 path passenv = TWINE_PASSWORD setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import path; path.Path('dist').rmtree_p()" python -m pep517.build . python -m twine upload dist/*