Import Upstream version 18.0.1
This commit is contained in:
commit
c04c0aee4a
|
@ -0,0 +1,6 @@
|
|||
[flake8]
|
||||
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
|
|
@ -0,0 +1,59 @@
|
|||
# Created by https://www.gitignore.io
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
python:
|
||||
version: 3
|
||||
extra_requirements:
|
||||
- docs
|
||||
pip_install: true
|
|
@ -0,0 +1,4 @@
|
|||
# Perform the manual steps on osx to install python and activate an environment
|
||||
brew update
|
||||
brew upgrade python
|
||||
export PATH=/usr/local/opt/python/libexec/bin:$PATH
|
|
@ -0,0 +1,40 @@
|
|||
dist: xenial
|
||||
sudo: false
|
||||
language: python
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
- &latest_py3 3.7
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
include:
|
||||
- os: osx
|
||||
language: generic
|
||||
python: *latest_py3
|
||||
- stage: deploy
|
||||
if: tag IS present
|
||||
python: *latest_py3
|
||||
before_script: skip
|
||||
env:
|
||||
- TWINE_USERNAME=jaraco
|
||||
# TWINE_PASSWORD
|
||||
- secure: VXM9F3HLDPprEY3JRlX6eH+yngucMHrqpUMeo4zgnFOc5yJUs7RP3uJ87O9IHllOUc7VR+nu/ye7HJk9y8Ux8X53S5UiH/s3/7rHPWP5IyCZBuDMZ3ouSxYKCWyMd6h/tkmVjNJIkV/0ZUhTJlwaODKwqegZ0twEIoRQnVIDTcs=
|
||||
- TOX_TESTENV_PASSENV="TWINE_USERNAME TWINE_PASSWORD"
|
||||
script: tox -e release
|
||||
|
||||
cache: pip
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then source .travis-macos; fi
|
||||
|
||||
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
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
|||
Copyright 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.
|
|
@ -0,0 +1,413 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: keyring
|
||||
Version: 18.0.1
|
||||
Summary: Store and access your passwords safely.
|
||||
Home-page: https://github.com/jaraco/keyring
|
||||
Author: Kang Zhang
|
||||
Author-email: jobo.zh@gmail.com
|
||||
Maintainer: Jason R. Coombs
|
||||
Maintainer-email: jaraco@jaraco.com
|
||||
License: UNKNOWN
|
||||
Description: .. image:: https://img.shields.io/pypi/v/keyring.svg
|
||||
:target: https://pypi.org/project/keyring
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/keyring.svg
|
||||
|
||||
.. image:: https://img.shields.io/travis/jaraco/keyring/master.svg
|
||||
:target: https://travis-ci.org/jaraco/keyring
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/jaraco/keyring/master.svg
|
||||
:target: https://ci.appveyor.com/project/jaraco/keyring/branch/master
|
||||
|
||||
.. image:: https://readthedocs.org/projects/keyring/badge/?version=latest
|
||||
:target: https://keyring.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://tidelift.com/badges/github/jaraco/keyring
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-keyring?utm_source=pypi-keyring&utm_medium=readme
|
||||
|
||||
=======================================
|
||||
Installing and Using Python Keyring Lib
|
||||
=======================================
|
||||
|
||||
.. contents:: **Table of Contents**
|
||||
|
||||
---------------------------
|
||||
What is Python keyring lib?
|
||||
---------------------------
|
||||
|
||||
The Python keyring lib provides an easy way to access the system keyring service
|
||||
from python. It can be used in any application that needs safe password storage.
|
||||
|
||||
The keyring library is licensed under both the `MIT license
|
||||
<http://opensource.org/licenses/MIT>`_ and the PSF license.
|
||||
|
||||
These recommended keyring backends are supported by the Python keyring lib:
|
||||
|
||||
* macOS `Keychain
|
||||
<https://en.wikipedia.org/wiki/Keychain_%28software%29>`_
|
||||
* Freedesktop `Secret Service
|
||||
<http://standards.freedesktop.org/secret-service/>`_ supports many DE including
|
||||
GNOME (requires `secretstorage <https://pypi.python.org/pypi/secretstorage>`_)
|
||||
* KDE4 & KDE5 `KWallet <https://en.wikipedia.org/wiki/KWallet>`_
|
||||
(requires `dbus <https://pypi.python.org/pypi/dbus-python>`_)
|
||||
* `Windows Credential Locker
|
||||
<https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_
|
||||
|
||||
Other keyring implementations are available through `Third-Party Backends`_.
|
||||
|
||||
-------------------------
|
||||
Installation Instructions
|
||||
-------------------------
|
||||
|
||||
Install from Index
|
||||
==================
|
||||
|
||||
Install using your favorite installer. For example:
|
||||
|
||||
$ pip install keyring
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
On Linux, the KWallet backend relies on dbus-python_, which does not always
|
||||
install correctly when using pip (compilation is needed). So we recommend
|
||||
that dbus-python is installed as a system package. The same also applies to
|
||||
the Secret Storage backend under Python 2 (under Python 3 a different D-Bus
|
||||
implementation is used).
|
||||
|
||||
.. _dbus-python: https://gitlab.freedesktop.org/dbus/dbus-python
|
||||
|
||||
-------------
|
||||
Using Keyring
|
||||
-------------
|
||||
|
||||
The basic usage of keyring is pretty simple: just call `keyring.set_password`
|
||||
and `keyring.get_password`:
|
||||
|
||||
>>> import keyring
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
Command-line Utility
|
||||
====================
|
||||
|
||||
Keyring supplies a ``keyring`` command which is installed with the
|
||||
package. After installing keyring in most environments, the
|
||||
command should be available for setting, getting, and deleting
|
||||
passwords. For more information on usage, invoke with no arguments
|
||||
or with ``--help`` as so::
|
||||
|
||||
$ keyring --help
|
||||
$ keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ keyring get system username
|
||||
password
|
||||
|
||||
The command-line functionality is also exposed as an executable
|
||||
package, suitable for invoking from Python like so::
|
||||
|
||||
$ python -m keyring --help
|
||||
$ python -m keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ python -m keyring get system username
|
||||
password
|
||||
|
||||
--------------------------
|
||||
Configure your keyring lib
|
||||
--------------------------
|
||||
|
||||
The python keyring lib contains implementations for several backends. The
|
||||
library will
|
||||
automatically choose the keyring that is most suitable for your current
|
||||
environment. You can also specify the keyring you like to be used in the
|
||||
config file or by calling the ``set_keyring()`` function.
|
||||
|
||||
Customize your keyring by config file
|
||||
=====================================
|
||||
|
||||
This section describes how to change your option in the config file.
|
||||
|
||||
Config file path
|
||||
----------------
|
||||
|
||||
The configuration of the lib is stored in a file named "keyringrc.cfg". This
|
||||
file must be found in a platform-specific location. To determine
|
||||
where the config file is stored, run the following::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.config_root())"
|
||||
|
||||
Some keyrings also store the keyring data in the file system. To determine
|
||||
where the data files are stored, run this command::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.data_root())"
|
||||
|
||||
|
||||
Config file content
|
||||
-------------------
|
||||
|
||||
To specify a keyring backend, set the **default-keyring** option to the
|
||||
full path of the class for that backend, such as
|
||||
``keyring.backends.OS_X.Keyring``.
|
||||
|
||||
If **keyring-path** is indicated, keyring will add that path to the Python
|
||||
module search path before loading the backend.
|
||||
|
||||
For example, this config might be used to load the
|
||||
``SimpleKeyring`` from the ``simplekeyring`` module in
|
||||
the ``./demo`` directory (not implemented)::
|
||||
|
||||
[backend]
|
||||
default-keyring=simplekeyring.SimpleKeyring
|
||||
keyring-path=demo
|
||||
|
||||
Third-Party Backends
|
||||
====================
|
||||
|
||||
In addition to the backends provided by the core keyring package for
|
||||
the most common and secure use cases, there
|
||||
are additional keyring backend implementations available for other
|
||||
use-cases. Simply install them to make them available:
|
||||
|
||||
- `keyrings.cryptfile <https://pypi.org/project/keyrings.cryptfile>`_
|
||||
- Encrypted text file storage.
|
||||
- `keyring_jeepney <https://pypi.org/project/keyring_jeepney>`__ - a
|
||||
pure Python backend using the secret service DBus API for desktop
|
||||
Linux.
|
||||
- `keyrings.alt <https://pypi.org/project/keyrings.alt>`_ - "alternate",
|
||||
possibly-insecure backends, originally part of the core package, but
|
||||
available for opt-in.
|
||||
- `gsheet-keyring <https://pypi.org/project/gsheet-keyring>`_
|
||||
- a backend that stores secrets in a Google Sheet. For use with
|
||||
`ipython-secrets <https://pypi.org/project/ipython-secrets>`_.
|
||||
- `bitwarden-keyring <https://pypi.org/project/bitwarden-keyring/0.1.0/>`_
|
||||
- a backend that stores secrets in the `BitWarden <https://bitwarden.com/>`_
|
||||
password manager.
|
||||
|
||||
|
||||
Write your own keyring backend
|
||||
==============================
|
||||
|
||||
The interface for the backend is defined by ``keyring.backend.KeyringBackend``.
|
||||
Every backend should derive from that base class and define a ``priority``
|
||||
attribute and three functions: ``get_password()``, ``set_password()``, and
|
||||
``delete_password()``. The ``get_credential()`` function may be defined if
|
||||
desired.
|
||||
|
||||
See the ``backend`` module for more detail on the interface of this class.
|
||||
|
||||
Keyring employs entry points to allow any third-party package to implement
|
||||
backends without any modification to the keyring itself. Those interested in
|
||||
creating new backends are encouraged to create new, third-party packages
|
||||
in the ``keyrings`` namespace, in a manner modeled by the `keyrings.alt
|
||||
package <https://github.com/jaraco/keyrings.alt>`_. See the ``setup.py`` file
|
||||
in that project for a hint on how to create the requisite entry points.
|
||||
Backends that prove essential may be considered for inclusion in the core
|
||||
library, although the ease of installing these third-party packages should
|
||||
mean that extensions may be readily available.
|
||||
|
||||
If you've created an extension for Keyring, please submit a pull request to
|
||||
have your extension mentioned as an available extension.
|
||||
|
||||
Set the keyring in runtime
|
||||
==========================
|
||||
|
||||
Keyring additionally allows programmatic configuration of the
|
||||
backend calling the api ``set_keyring()``. The indicated backend
|
||||
will subsequently be used to store and retrieve passwords.
|
||||
|
||||
Here's an example demonstrating how to invoke ``set_keyring``::
|
||||
|
||||
# define a new keyring class which extends the KeyringBackend
|
||||
import keyring.backend
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring which always outputs same password
|
||||
"""
|
||||
priority = 1
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
return "password from TestKeyring"
|
||||
|
||||
def delete_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
# set the keyring for keyring lib
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
# invoke the keyring lib
|
||||
try:
|
||||
keyring.set_password("demo-service", "tarek", "passexample")
|
||||
print("password stored successfully")
|
||||
except keyring.errors.PasswordSetError:
|
||||
print("failed to store password")
|
||||
print("password", keyring.get_password("demo-service", "tarek"))
|
||||
|
||||
|
||||
Using Keyring on Ubuntu 16.04
|
||||
=============================
|
||||
|
||||
The following is a complete transcript for installing keyring in a
|
||||
virtual environment on Ubuntu 16.04. No config file was used.::
|
||||
|
||||
$ sudo apt install python3-venv libdbus-glib-1-dev
|
||||
$ cd /tmp
|
||||
$ pyvenv py3
|
||||
$ source py3/bin/activate
|
||||
$ pip install -U pip
|
||||
$ pip install secretstorage dbus-python
|
||||
$ pip install keyring
|
||||
$ python
|
||||
>>> import keyring
|
||||
>>> keyring.get_keyring()
|
||||
<keyring.backends.SecretService.Keyring object at 0x7f9b9c971ba8>
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
|
||||
Using Keyring on headless Linux systems
|
||||
=======================================
|
||||
|
||||
It is possible to use the SecretService backend on Linux systems without
|
||||
X11 server available (only D-Bus is required). To do that, you need the
|
||||
following:
|
||||
|
||||
* Install the `GNOME Keyring`_ daemon.
|
||||
* Start a D-Bus session, e.g. run ``dbus-run-session -- sh`` and run
|
||||
the following commands inside that shell.
|
||||
* Run ``gnome-keyring-daemon`` with ``--unlock`` option. The description of
|
||||
that option says:
|
||||
|
||||
Read a password from stdin, and use it to unlock the login keyring
|
||||
or create it if the login keyring does not exist.
|
||||
|
||||
When that command is started, enter your password into stdin and
|
||||
press Ctrl+D (end of data). After that the daemon will fork into
|
||||
background (use ``--foreground`` option to prevent that).
|
||||
* Now you can use the SecretService backend of Keyring. Remember to
|
||||
run your application in the same D-Bus session as the daemon.
|
||||
|
||||
.. _GNOME Keyring: https://wiki.gnome.org/Projects/GnomeKeyring
|
||||
|
||||
-----------------------------------------------
|
||||
Integrate the keyring lib with your application
|
||||
-----------------------------------------------
|
||||
|
||||
API interface
|
||||
=============
|
||||
|
||||
The keyring lib has a few functions:
|
||||
|
||||
* ``get_keyring()``: Return the currently-loaded keyring implementation.
|
||||
* ``get_password(service, username)``: Returns the password stored in the
|
||||
active keyring. If the password does not exist, it will return None.
|
||||
* ``get_credential(service, username)``: Return a credential object stored
|
||||
in the active keyring. This object contains at least ``username`` and
|
||||
``password`` attributes for the specified service, where the returned
|
||||
``username`` may be different from the argument.
|
||||
* ``set_password(service, username, password)``: Store the password in the
|
||||
keyring.
|
||||
* ``delete_password(service, username)``: Delete the password stored in
|
||||
keyring. If the password does not exist, it will raise an exception.
|
||||
|
||||
In all cases, the parameters (``service``, ``username``, ``password``)
|
||||
should be Unicode text. On Python 2, these parameters are accepted as
|
||||
simple ``str`` in the default encoding as they will be implicitly
|
||||
decoded to text. Some backends may accept ``bytes`` for these parameters,
|
||||
but such usage is discouraged.
|
||||
|
||||
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
The keyring lib raises following exceptions:
|
||||
|
||||
* ``keyring.errors.KeyringError``: Base Error class for all exceptions in keyring lib.
|
||||
* ``keyring.errors.InitError``: Raised when the keyring can't be initialized.
|
||||
* ``keyring.errors.PasswordSetError``: Raise when password can't be set in the keyring.
|
||||
* ``keyring.errors.PasswordDeleteError``: Raised when the password can't be deleted in the keyring.
|
||||
|
||||
------------
|
||||
Get involved
|
||||
------------
|
||||
|
||||
Python keyring lib is an open community project and highly welcomes new
|
||||
contributors.
|
||||
|
||||
* Repository: https://github.com/jaraco/keyring/
|
||||
* Bug Tracker: https://github.com/jaraco/keyring/issues/
|
||||
* Mailing list: http://groups.google.com/group/python-keyring
|
||||
|
||||
Security Contact
|
||||
================
|
||||
|
||||
If you wish to report a security vulnerability, the public disclosure
|
||||
of which may exacerbate the risk, please
|
||||
`Contact Tidelift security <https://tidelift.com/security>`_,
|
||||
which will coordinate the fix and disclosure privately.
|
||||
|
||||
Making Releases
|
||||
===============
|
||||
|
||||
This project makes use of automated releases via Travis-CI. The
|
||||
simple workflow is to tag a commit and push it to Github. If it
|
||||
passes tests on a late Python version, it will be automatically
|
||||
deployed to PyPI.
|
||||
|
||||
Other things to consider when making a release:
|
||||
|
||||
- first ensure that tests pass (preferably on Windows and Linux)
|
||||
- check that the changelog is current for the intended release
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Tests are `continuously run <https://travis-ci.org/#!/jaraco/keyring>`_ using
|
||||
Travis-CI.
|
||||
|
||||
To run the tests yourself, you'll want keyring installed to some environment
|
||||
in which it can be tested. Recommended technique is described below.
|
||||
|
||||
Using tox
|
||||
---------
|
||||
|
||||
Keyring prefers use of `tox <https://pypi.org/project/tox>`_ to run tests.
|
||||
Simply install and invoke ``tox``.
|
||||
|
||||
This technique is the one used by the Travis-CI script.
|
||||
|
||||
----------
|
||||
Background
|
||||
----------
|
||||
|
||||
The project was based on Tarek Ziade's idea in `this post`_. Kang Zhang
|
||||
initially carried it out as a `Google Summer of Code`_ project, and Tarek
|
||||
mentored Kang on this project.
|
||||
|
||||
.. _this post: http://tarekziade.wordpress.com/2009/03/27/pycon-hallway-session-1-a-keyring-library-for-python/
|
||||
.. _Google Summer of Code: http://socghop.appspot.com/
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/jaraco/keyring.svg
|
||||
:alt: Join the chat at https://gitter.im/jaraco/keyring
|
||||
:target: https://gitter.im/jaraco/keyring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Requires-Python: >=2.7
|
||||
Provides-Extra: docs
|
||||
Provides-Extra: testing
|
|
@ -0,0 +1,387 @@
|
|||
.. image:: https://img.shields.io/pypi/v/keyring.svg
|
||||
:target: https://pypi.org/project/keyring
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/keyring.svg
|
||||
|
||||
.. image:: https://img.shields.io/travis/jaraco/keyring/master.svg
|
||||
:target: https://travis-ci.org/jaraco/keyring
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/jaraco/keyring/master.svg
|
||||
:target: https://ci.appveyor.com/project/jaraco/keyring/branch/master
|
||||
|
||||
.. image:: https://readthedocs.org/projects/keyring/badge/?version=latest
|
||||
:target: https://keyring.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://tidelift.com/badges/github/jaraco/keyring
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-keyring?utm_source=pypi-keyring&utm_medium=readme
|
||||
|
||||
=======================================
|
||||
Installing and Using Python Keyring Lib
|
||||
=======================================
|
||||
|
||||
.. contents:: **Table of Contents**
|
||||
|
||||
---------------------------
|
||||
What is Python keyring lib?
|
||||
---------------------------
|
||||
|
||||
The Python keyring lib provides an easy way to access the system keyring service
|
||||
from python. It can be used in any application that needs safe password storage.
|
||||
|
||||
The keyring library is licensed under both the `MIT license
|
||||
<http://opensource.org/licenses/MIT>`_ and the PSF license.
|
||||
|
||||
These recommended keyring backends are supported by the Python keyring lib:
|
||||
|
||||
* macOS `Keychain
|
||||
<https://en.wikipedia.org/wiki/Keychain_%28software%29>`_
|
||||
* Freedesktop `Secret Service
|
||||
<http://standards.freedesktop.org/secret-service/>`_ supports many DE including
|
||||
GNOME (requires `secretstorage <https://pypi.python.org/pypi/secretstorage>`_)
|
||||
* KDE4 & KDE5 `KWallet <https://en.wikipedia.org/wiki/KWallet>`_
|
||||
(requires `dbus <https://pypi.python.org/pypi/dbus-python>`_)
|
||||
* `Windows Credential Locker
|
||||
<https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_
|
||||
|
||||
Other keyring implementations are available through `Third-Party Backends`_.
|
||||
|
||||
-------------------------
|
||||
Installation Instructions
|
||||
-------------------------
|
||||
|
||||
Install from Index
|
||||
==================
|
||||
|
||||
Install using your favorite installer. For example:
|
||||
|
||||
$ pip install keyring
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
On Linux, the KWallet backend relies on dbus-python_, which does not always
|
||||
install correctly when using pip (compilation is needed). So we recommend
|
||||
that dbus-python is installed as a system package. The same also applies to
|
||||
the Secret Storage backend under Python 2 (under Python 3 a different D-Bus
|
||||
implementation is used).
|
||||
|
||||
.. _dbus-python: https://gitlab.freedesktop.org/dbus/dbus-python
|
||||
|
||||
-------------
|
||||
Using Keyring
|
||||
-------------
|
||||
|
||||
The basic usage of keyring is pretty simple: just call `keyring.set_password`
|
||||
and `keyring.get_password`:
|
||||
|
||||
>>> import keyring
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
Command-line Utility
|
||||
====================
|
||||
|
||||
Keyring supplies a ``keyring`` command which is installed with the
|
||||
package. After installing keyring in most environments, the
|
||||
command should be available for setting, getting, and deleting
|
||||
passwords. For more information on usage, invoke with no arguments
|
||||
or with ``--help`` as so::
|
||||
|
||||
$ keyring --help
|
||||
$ keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ keyring get system username
|
||||
password
|
||||
|
||||
The command-line functionality is also exposed as an executable
|
||||
package, suitable for invoking from Python like so::
|
||||
|
||||
$ python -m keyring --help
|
||||
$ python -m keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ python -m keyring get system username
|
||||
password
|
||||
|
||||
--------------------------
|
||||
Configure your keyring lib
|
||||
--------------------------
|
||||
|
||||
The python keyring lib contains implementations for several backends. The
|
||||
library will
|
||||
automatically choose the keyring that is most suitable for your current
|
||||
environment. You can also specify the keyring you like to be used in the
|
||||
config file or by calling the ``set_keyring()`` function.
|
||||
|
||||
Customize your keyring by config file
|
||||
=====================================
|
||||
|
||||
This section describes how to change your option in the config file.
|
||||
|
||||
Config file path
|
||||
----------------
|
||||
|
||||
The configuration of the lib is stored in a file named "keyringrc.cfg". This
|
||||
file must be found in a platform-specific location. To determine
|
||||
where the config file is stored, run the following::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.config_root())"
|
||||
|
||||
Some keyrings also store the keyring data in the file system. To determine
|
||||
where the data files are stored, run this command::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.data_root())"
|
||||
|
||||
|
||||
Config file content
|
||||
-------------------
|
||||
|
||||
To specify a keyring backend, set the **default-keyring** option to the
|
||||
full path of the class for that backend, such as
|
||||
``keyring.backends.OS_X.Keyring``.
|
||||
|
||||
If **keyring-path** is indicated, keyring will add that path to the Python
|
||||
module search path before loading the backend.
|
||||
|
||||
For example, this config might be used to load the
|
||||
``SimpleKeyring`` from the ``simplekeyring`` module in
|
||||
the ``./demo`` directory (not implemented)::
|
||||
|
||||
[backend]
|
||||
default-keyring=simplekeyring.SimpleKeyring
|
||||
keyring-path=demo
|
||||
|
||||
Third-Party Backends
|
||||
====================
|
||||
|
||||
In addition to the backends provided by the core keyring package for
|
||||
the most common and secure use cases, there
|
||||
are additional keyring backend implementations available for other
|
||||
use-cases. Simply install them to make them available:
|
||||
|
||||
- `keyrings.cryptfile <https://pypi.org/project/keyrings.cryptfile>`_
|
||||
- Encrypted text file storage.
|
||||
- `keyring_jeepney <https://pypi.org/project/keyring_jeepney>`__ - a
|
||||
pure Python backend using the secret service DBus API for desktop
|
||||
Linux.
|
||||
- `keyrings.alt <https://pypi.org/project/keyrings.alt>`_ - "alternate",
|
||||
possibly-insecure backends, originally part of the core package, but
|
||||
available for opt-in.
|
||||
- `gsheet-keyring <https://pypi.org/project/gsheet-keyring>`_
|
||||
- a backend that stores secrets in a Google Sheet. For use with
|
||||
`ipython-secrets <https://pypi.org/project/ipython-secrets>`_.
|
||||
- `bitwarden-keyring <https://pypi.org/project/bitwarden-keyring/0.1.0/>`_
|
||||
- a backend that stores secrets in the `BitWarden <https://bitwarden.com/>`_
|
||||
password manager.
|
||||
|
||||
|
||||
Write your own keyring backend
|
||||
==============================
|
||||
|
||||
The interface for the backend is defined by ``keyring.backend.KeyringBackend``.
|
||||
Every backend should derive from that base class and define a ``priority``
|
||||
attribute and three functions: ``get_password()``, ``set_password()``, and
|
||||
``delete_password()``. The ``get_credential()`` function may be defined if
|
||||
desired.
|
||||
|
||||
See the ``backend`` module for more detail on the interface of this class.
|
||||
|
||||
Keyring employs entry points to allow any third-party package to implement
|
||||
backends without any modification to the keyring itself. Those interested in
|
||||
creating new backends are encouraged to create new, third-party packages
|
||||
in the ``keyrings`` namespace, in a manner modeled by the `keyrings.alt
|
||||
package <https://github.com/jaraco/keyrings.alt>`_. See the ``setup.py`` file
|
||||
in that project for a hint on how to create the requisite entry points.
|
||||
Backends that prove essential may be considered for inclusion in the core
|
||||
library, although the ease of installing these third-party packages should
|
||||
mean that extensions may be readily available.
|
||||
|
||||
If you've created an extension for Keyring, please submit a pull request to
|
||||
have your extension mentioned as an available extension.
|
||||
|
||||
Set the keyring in runtime
|
||||
==========================
|
||||
|
||||
Keyring additionally allows programmatic configuration of the
|
||||
backend calling the api ``set_keyring()``. The indicated backend
|
||||
will subsequently be used to store and retrieve passwords.
|
||||
|
||||
Here's an example demonstrating how to invoke ``set_keyring``::
|
||||
|
||||
# define a new keyring class which extends the KeyringBackend
|
||||
import keyring.backend
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring which always outputs same password
|
||||
"""
|
||||
priority = 1
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
return "password from TestKeyring"
|
||||
|
||||
def delete_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
# set the keyring for keyring lib
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
# invoke the keyring lib
|
||||
try:
|
||||
keyring.set_password("demo-service", "tarek", "passexample")
|
||||
print("password stored successfully")
|
||||
except keyring.errors.PasswordSetError:
|
||||
print("failed to store password")
|
||||
print("password", keyring.get_password("demo-service", "tarek"))
|
||||
|
||||
|
||||
Using Keyring on Ubuntu 16.04
|
||||
=============================
|
||||
|
||||
The following is a complete transcript for installing keyring in a
|
||||
virtual environment on Ubuntu 16.04. No config file was used.::
|
||||
|
||||
$ sudo apt install python3-venv libdbus-glib-1-dev
|
||||
$ cd /tmp
|
||||
$ pyvenv py3
|
||||
$ source py3/bin/activate
|
||||
$ pip install -U pip
|
||||
$ pip install secretstorage dbus-python
|
||||
$ pip install keyring
|
||||
$ python
|
||||
>>> import keyring
|
||||
>>> keyring.get_keyring()
|
||||
<keyring.backends.SecretService.Keyring object at 0x7f9b9c971ba8>
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
|
||||
Using Keyring on headless Linux systems
|
||||
=======================================
|
||||
|
||||
It is possible to use the SecretService backend on Linux systems without
|
||||
X11 server available (only D-Bus is required). To do that, you need the
|
||||
following:
|
||||
|
||||
* Install the `GNOME Keyring`_ daemon.
|
||||
* Start a D-Bus session, e.g. run ``dbus-run-session -- sh`` and run
|
||||
the following commands inside that shell.
|
||||
* Run ``gnome-keyring-daemon`` with ``--unlock`` option. The description of
|
||||
that option says:
|
||||
|
||||
Read a password from stdin, and use it to unlock the login keyring
|
||||
or create it if the login keyring does not exist.
|
||||
|
||||
When that command is started, enter your password into stdin and
|
||||
press Ctrl+D (end of data). After that the daemon will fork into
|
||||
background (use ``--foreground`` option to prevent that).
|
||||
* Now you can use the SecretService backend of Keyring. Remember to
|
||||
run your application in the same D-Bus session as the daemon.
|
||||
|
||||
.. _GNOME Keyring: https://wiki.gnome.org/Projects/GnomeKeyring
|
||||
|
||||
-----------------------------------------------
|
||||
Integrate the keyring lib with your application
|
||||
-----------------------------------------------
|
||||
|
||||
API interface
|
||||
=============
|
||||
|
||||
The keyring lib has a few functions:
|
||||
|
||||
* ``get_keyring()``: Return the currently-loaded keyring implementation.
|
||||
* ``get_password(service, username)``: Returns the password stored in the
|
||||
active keyring. If the password does not exist, it will return None.
|
||||
* ``get_credential(service, username)``: Return a credential object stored
|
||||
in the active keyring. This object contains at least ``username`` and
|
||||
``password`` attributes for the specified service, where the returned
|
||||
``username`` may be different from the argument.
|
||||
* ``set_password(service, username, password)``: Store the password in the
|
||||
keyring.
|
||||
* ``delete_password(service, username)``: Delete the password stored in
|
||||
keyring. If the password does not exist, it will raise an exception.
|
||||
|
||||
In all cases, the parameters (``service``, ``username``, ``password``)
|
||||
should be Unicode text. On Python 2, these parameters are accepted as
|
||||
simple ``str`` in the default encoding as they will be implicitly
|
||||
decoded to text. Some backends may accept ``bytes`` for these parameters,
|
||||
but such usage is discouraged.
|
||||
|
||||
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
The keyring lib raises following exceptions:
|
||||
|
||||
* ``keyring.errors.KeyringError``: Base Error class for all exceptions in keyring lib.
|
||||
* ``keyring.errors.InitError``: Raised when the keyring can't be initialized.
|
||||
* ``keyring.errors.PasswordSetError``: Raise when password can't be set in the keyring.
|
||||
* ``keyring.errors.PasswordDeleteError``: Raised when the password can't be deleted in the keyring.
|
||||
|
||||
------------
|
||||
Get involved
|
||||
------------
|
||||
|
||||
Python keyring lib is an open community project and highly welcomes new
|
||||
contributors.
|
||||
|
||||
* Repository: https://github.com/jaraco/keyring/
|
||||
* Bug Tracker: https://github.com/jaraco/keyring/issues/
|
||||
* Mailing list: http://groups.google.com/group/python-keyring
|
||||
|
||||
Security Contact
|
||||
================
|
||||
|
||||
If you wish to report a security vulnerability, the public disclosure
|
||||
of which may exacerbate the risk, please
|
||||
`Contact Tidelift security <https://tidelift.com/security>`_,
|
||||
which will coordinate the fix and disclosure privately.
|
||||
|
||||
Making Releases
|
||||
===============
|
||||
|
||||
This project makes use of automated releases via Travis-CI. The
|
||||
simple workflow is to tag a commit and push it to Github. If it
|
||||
passes tests on a late Python version, it will be automatically
|
||||
deployed to PyPI.
|
||||
|
||||
Other things to consider when making a release:
|
||||
|
||||
- first ensure that tests pass (preferably on Windows and Linux)
|
||||
- check that the changelog is current for the intended release
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Tests are `continuously run <https://travis-ci.org/#!/jaraco/keyring>`_ using
|
||||
Travis-CI.
|
||||
|
||||
To run the tests yourself, you'll want keyring installed to some environment
|
||||
in which it can be tested. Recommended technique is described below.
|
||||
|
||||
Using tox
|
||||
---------
|
||||
|
||||
Keyring prefers use of `tox <https://pypi.org/project/tox>`_ to run tests.
|
||||
Simply install and invoke ``tox``.
|
||||
|
||||
This technique is the one used by the Travis-CI script.
|
||||
|
||||
----------
|
||||
Background
|
||||
----------
|
||||
|
||||
The project was based on Tarek Ziade's idea in `this post`_. Kang Zhang
|
||||
initially carried it out as a `Google Summer of Code`_ project, and Tarek
|
||||
mentored Kang on this project.
|
||||
|
||||
.. _this post: http://tarekziade.wordpress.com/2009/03/27/pycon-hallway-session-1-a-keyring-library-for-python/
|
||||
.. _Google Summer of Code: http://socghop.appspot.com/
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/jaraco/keyring.svg
|
||||
:alt: Join the chat at https://gitter.im/jaraco/keyring
|
||||
:target: https://gitter.im/jaraco/keyring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
|
@ -0,0 +1,24 @@
|
|||
environment:
|
||||
|
||||
APPVEYOR: true
|
||||
|
||||
matrix:
|
||||
- PYTHON: "C:\\Python36-x64"
|
||||
- PYTHON: "C:\\Python27-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 tox tox-venv"
|
||||
- "tox"
|
||||
|
||||
version: '{build}'
|
|
@ -0,0 +1,10 @@
|
|||
import platform
|
||||
|
||||
collect_ignore = [
|
||||
"hook-keyring.backend.py",
|
||||
]
|
||||
|
||||
if platform.system() != 'Darwin':
|
||||
collect_ignore.append('keyring/backends/_OS_X_API.py')
|
||||
|
||||
collect_ignore.append('keyring/devpi_client.py')
|
|
@ -0,0 +1,6 @@
|
|||
<h3 class="donation">Professional support</h3>
|
||||
|
||||
<p>
|
||||
Professionally-supported {{ project }} is available with the
|
||||
<a href="https://tidelift.com/subscription/pkg/pypi-{{ project }}?utm_source=pypi-{{ project }}&utm_medium=readme">Tidelift Subscription</a>.
|
||||
</p>
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
|
||||
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<issue>\d+)",
|
||||
url="{package_url}/issues/{issue}",
|
||||
),
|
||||
dict(
|
||||
pattern=r"^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n",
|
||||
with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n",
|
||||
),
|
||||
dict(
|
||||
pattern=r"PEP[- ](?P<pep_number>\d+)",
|
||||
url="https://www.python.org/dev/peps/pep-{pep_number:0>4}/",
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_theme = 'alabaster'
|
||||
templates_path = ['_templates']
|
||||
html_sidebars = {'index': 'tidelift-sidebar.html'}
|
|
@ -0,0 +1,8 @@
|
|||
:tocdepth: 2
|
||||
|
||||
.. _changes:
|
||||
|
||||
History
|
||||
*******
|
||||
|
||||
.. include:: ../CHANGES (links).rst
|
|
@ -0,0 +1,24 @@
|
|||
Welcome to keyring documentation!
|
||||
=================================
|
||||
|
||||
.. include:: ../README.rst
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
history
|
||||
|
||||
|
||||
.. automodule:: keyring
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Used by pyinstaller to expose hidden imports
|
||||
|
||||
import entrypoints
|
||||
|
||||
|
||||
hiddenimports = [
|
||||
ep.module_name
|
||||
for ep in entrypoints.get_group_all('keyring.backends')
|
||||
]
|
|
@ -0,0 +1,413 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: keyring
|
||||
Version: 18.0.1
|
||||
Summary: Store and access your passwords safely.
|
||||
Home-page: https://github.com/jaraco/keyring
|
||||
Author: Kang Zhang
|
||||
Author-email: jobo.zh@gmail.com
|
||||
Maintainer: Jason R. Coombs
|
||||
Maintainer-email: jaraco@jaraco.com
|
||||
License: UNKNOWN
|
||||
Description: .. image:: https://img.shields.io/pypi/v/keyring.svg
|
||||
:target: https://pypi.org/project/keyring
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/keyring.svg
|
||||
|
||||
.. image:: https://img.shields.io/travis/jaraco/keyring/master.svg
|
||||
:target: https://travis-ci.org/jaraco/keyring
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/jaraco/keyring/master.svg
|
||||
:target: https://ci.appveyor.com/project/jaraco/keyring/branch/master
|
||||
|
||||
.. image:: https://readthedocs.org/projects/keyring/badge/?version=latest
|
||||
:target: https://keyring.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://tidelift.com/badges/github/jaraco/keyring
|
||||
:target: https://tidelift.com/subscription/pkg/pypi-keyring?utm_source=pypi-keyring&utm_medium=readme
|
||||
|
||||
=======================================
|
||||
Installing and Using Python Keyring Lib
|
||||
=======================================
|
||||
|
||||
.. contents:: **Table of Contents**
|
||||
|
||||
---------------------------
|
||||
What is Python keyring lib?
|
||||
---------------------------
|
||||
|
||||
The Python keyring lib provides an easy way to access the system keyring service
|
||||
from python. It can be used in any application that needs safe password storage.
|
||||
|
||||
The keyring library is licensed under both the `MIT license
|
||||
<http://opensource.org/licenses/MIT>`_ and the PSF license.
|
||||
|
||||
These recommended keyring backends are supported by the Python keyring lib:
|
||||
|
||||
* macOS `Keychain
|
||||
<https://en.wikipedia.org/wiki/Keychain_%28software%29>`_
|
||||
* Freedesktop `Secret Service
|
||||
<http://standards.freedesktop.org/secret-service/>`_ supports many DE including
|
||||
GNOME (requires `secretstorage <https://pypi.python.org/pypi/secretstorage>`_)
|
||||
* KDE4 & KDE5 `KWallet <https://en.wikipedia.org/wiki/KWallet>`_
|
||||
(requires `dbus <https://pypi.python.org/pypi/dbus-python>`_)
|
||||
* `Windows Credential Locker
|
||||
<https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_
|
||||
|
||||
Other keyring implementations are available through `Third-Party Backends`_.
|
||||
|
||||
-------------------------
|
||||
Installation Instructions
|
||||
-------------------------
|
||||
|
||||
Install from Index
|
||||
==================
|
||||
|
||||
Install using your favorite installer. For example:
|
||||
|
||||
$ pip install keyring
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
On Linux, the KWallet backend relies on dbus-python_, which does not always
|
||||
install correctly when using pip (compilation is needed). So we recommend
|
||||
that dbus-python is installed as a system package. The same also applies to
|
||||
the Secret Storage backend under Python 2 (under Python 3 a different D-Bus
|
||||
implementation is used).
|
||||
|
||||
.. _dbus-python: https://gitlab.freedesktop.org/dbus/dbus-python
|
||||
|
||||
-------------
|
||||
Using Keyring
|
||||
-------------
|
||||
|
||||
The basic usage of keyring is pretty simple: just call `keyring.set_password`
|
||||
and `keyring.get_password`:
|
||||
|
||||
>>> import keyring
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
Command-line Utility
|
||||
====================
|
||||
|
||||
Keyring supplies a ``keyring`` command which is installed with the
|
||||
package. After installing keyring in most environments, the
|
||||
command should be available for setting, getting, and deleting
|
||||
passwords. For more information on usage, invoke with no arguments
|
||||
or with ``--help`` as so::
|
||||
|
||||
$ keyring --help
|
||||
$ keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ keyring get system username
|
||||
password
|
||||
|
||||
The command-line functionality is also exposed as an executable
|
||||
package, suitable for invoking from Python like so::
|
||||
|
||||
$ python -m keyring --help
|
||||
$ python -m keyring set system username
|
||||
Password for 'username' in 'system':
|
||||
$ python -m keyring get system username
|
||||
password
|
||||
|
||||
--------------------------
|
||||
Configure your keyring lib
|
||||
--------------------------
|
||||
|
||||
The python keyring lib contains implementations for several backends. The
|
||||
library will
|
||||
automatically choose the keyring that is most suitable for your current
|
||||
environment. You can also specify the keyring you like to be used in the
|
||||
config file or by calling the ``set_keyring()`` function.
|
||||
|
||||
Customize your keyring by config file
|
||||
=====================================
|
||||
|
||||
This section describes how to change your option in the config file.
|
||||
|
||||
Config file path
|
||||
----------------
|
||||
|
||||
The configuration of the lib is stored in a file named "keyringrc.cfg". This
|
||||
file must be found in a platform-specific location. To determine
|
||||
where the config file is stored, run the following::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.config_root())"
|
||||
|
||||
Some keyrings also store the keyring data in the file system. To determine
|
||||
where the data files are stored, run this command::
|
||||
|
||||
python -c "import keyring.util.platform_; print(keyring.util.platform_.data_root())"
|
||||
|
||||
|
||||
Config file content
|
||||
-------------------
|
||||
|
||||
To specify a keyring backend, set the **default-keyring** option to the
|
||||
full path of the class for that backend, such as
|
||||
``keyring.backends.OS_X.Keyring``.
|
||||
|
||||
If **keyring-path** is indicated, keyring will add that path to the Python
|
||||
module search path before loading the backend.
|
||||
|
||||
For example, this config might be used to load the
|
||||
``SimpleKeyring`` from the ``simplekeyring`` module in
|
||||
the ``./demo`` directory (not implemented)::
|
||||
|
||||
[backend]
|
||||
default-keyring=simplekeyring.SimpleKeyring
|
||||
keyring-path=demo
|
||||
|
||||
Third-Party Backends
|
||||
====================
|
||||
|
||||
In addition to the backends provided by the core keyring package for
|
||||
the most common and secure use cases, there
|
||||
are additional keyring backend implementations available for other
|
||||
use-cases. Simply install them to make them available:
|
||||
|
||||
- `keyrings.cryptfile <https://pypi.org/project/keyrings.cryptfile>`_
|
||||
- Encrypted text file storage.
|
||||
- `keyring_jeepney <https://pypi.org/project/keyring_jeepney>`__ - a
|
||||
pure Python backend using the secret service DBus API for desktop
|
||||
Linux.
|
||||
- `keyrings.alt <https://pypi.org/project/keyrings.alt>`_ - "alternate",
|
||||
possibly-insecure backends, originally part of the core package, but
|
||||
available for opt-in.
|
||||
- `gsheet-keyring <https://pypi.org/project/gsheet-keyring>`_
|
||||
- a backend that stores secrets in a Google Sheet. For use with
|
||||
`ipython-secrets <https://pypi.org/project/ipython-secrets>`_.
|
||||
- `bitwarden-keyring <https://pypi.org/project/bitwarden-keyring/0.1.0/>`_
|
||||
- a backend that stores secrets in the `BitWarden <https://bitwarden.com/>`_
|
||||
password manager.
|
||||
|
||||
|
||||
Write your own keyring backend
|
||||
==============================
|
||||
|
||||
The interface for the backend is defined by ``keyring.backend.KeyringBackend``.
|
||||
Every backend should derive from that base class and define a ``priority``
|
||||
attribute and three functions: ``get_password()``, ``set_password()``, and
|
||||
``delete_password()``. The ``get_credential()`` function may be defined if
|
||||
desired.
|
||||
|
||||
See the ``backend`` module for more detail on the interface of this class.
|
||||
|
||||
Keyring employs entry points to allow any third-party package to implement
|
||||
backends without any modification to the keyring itself. Those interested in
|
||||
creating new backends are encouraged to create new, third-party packages
|
||||
in the ``keyrings`` namespace, in a manner modeled by the `keyrings.alt
|
||||
package <https://github.com/jaraco/keyrings.alt>`_. See the ``setup.py`` file
|
||||
in that project for a hint on how to create the requisite entry points.
|
||||
Backends that prove essential may be considered for inclusion in the core
|
||||
library, although the ease of installing these third-party packages should
|
||||
mean that extensions may be readily available.
|
||||
|
||||
If you've created an extension for Keyring, please submit a pull request to
|
||||
have your extension mentioned as an available extension.
|
||||
|
||||
Set the keyring in runtime
|
||||
==========================
|
||||
|
||||
Keyring additionally allows programmatic configuration of the
|
||||
backend calling the api ``set_keyring()``. The indicated backend
|
||||
will subsequently be used to store and retrieve passwords.
|
||||
|
||||
Here's an example demonstrating how to invoke ``set_keyring``::
|
||||
|
||||
# define a new keyring class which extends the KeyringBackend
|
||||
import keyring.backend
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring which always outputs same password
|
||||
"""
|
||||
priority = 1
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
return "password from TestKeyring"
|
||||
|
||||
def delete_password(self, servicename, username, password):
|
||||
pass
|
||||
|
||||
# set the keyring for keyring lib
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
# invoke the keyring lib
|
||||
try:
|
||||
keyring.set_password("demo-service", "tarek", "passexample")
|
||||
print("password stored successfully")
|
||||
except keyring.errors.PasswordSetError:
|
||||
print("failed to store password")
|
||||
print("password", keyring.get_password("demo-service", "tarek"))
|
||||
|
||||
|
||||
Using Keyring on Ubuntu 16.04
|
||||
=============================
|
||||
|
||||
The following is a complete transcript for installing keyring in a
|
||||
virtual environment on Ubuntu 16.04. No config file was used.::
|
||||
|
||||
$ sudo apt install python3-venv libdbus-glib-1-dev
|
||||
$ cd /tmp
|
||||
$ pyvenv py3
|
||||
$ source py3/bin/activate
|
||||
$ pip install -U pip
|
||||
$ pip install secretstorage dbus-python
|
||||
$ pip install keyring
|
||||
$ python
|
||||
>>> import keyring
|
||||
>>> keyring.get_keyring()
|
||||
<keyring.backends.SecretService.Keyring object at 0x7f9b9c971ba8>
|
||||
>>> keyring.set_password("system", "username", "password")
|
||||
>>> keyring.get_password("system", "username")
|
||||
'password'
|
||||
|
||||
|
||||
Using Keyring on headless Linux systems
|
||||
=======================================
|
||||
|
||||
It is possible to use the SecretService backend on Linux systems without
|
||||
X11 server available (only D-Bus is required). To do that, you need the
|
||||
following:
|
||||
|
||||
* Install the `GNOME Keyring`_ daemon.
|
||||
* Start a D-Bus session, e.g. run ``dbus-run-session -- sh`` and run
|
||||
the following commands inside that shell.
|
||||
* Run ``gnome-keyring-daemon`` with ``--unlock`` option. The description of
|
||||
that option says:
|
||||
|
||||
Read a password from stdin, and use it to unlock the login keyring
|
||||
or create it if the login keyring does not exist.
|
||||
|
||||
When that command is started, enter your password into stdin and
|
||||
press Ctrl+D (end of data). After that the daemon will fork into
|
||||
background (use ``--foreground`` option to prevent that).
|
||||
* Now you can use the SecretService backend of Keyring. Remember to
|
||||
run your application in the same D-Bus session as the daemon.
|
||||
|
||||
.. _GNOME Keyring: https://wiki.gnome.org/Projects/GnomeKeyring
|
||||
|
||||
-----------------------------------------------
|
||||
Integrate the keyring lib with your application
|
||||
-----------------------------------------------
|
||||
|
||||
API interface
|
||||
=============
|
||||
|
||||
The keyring lib has a few functions:
|
||||
|
||||
* ``get_keyring()``: Return the currently-loaded keyring implementation.
|
||||
* ``get_password(service, username)``: Returns the password stored in the
|
||||
active keyring. If the password does not exist, it will return None.
|
||||
* ``get_credential(service, username)``: Return a credential object stored
|
||||
in the active keyring. This object contains at least ``username`` and
|
||||
``password`` attributes for the specified service, where the returned
|
||||
``username`` may be different from the argument.
|
||||
* ``set_password(service, username, password)``: Store the password in the
|
||||
keyring.
|
||||
* ``delete_password(service, username)``: Delete the password stored in
|
||||
keyring. If the password does not exist, it will raise an exception.
|
||||
|
||||
In all cases, the parameters (``service``, ``username``, ``password``)
|
||||
should be Unicode text. On Python 2, these parameters are accepted as
|
||||
simple ``str`` in the default encoding as they will be implicitly
|
||||
decoded to text. Some backends may accept ``bytes`` for these parameters,
|
||||
but such usage is discouraged.
|
||||
|
||||
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
The keyring lib raises following exceptions:
|
||||
|
||||
* ``keyring.errors.KeyringError``: Base Error class for all exceptions in keyring lib.
|
||||
* ``keyring.errors.InitError``: Raised when the keyring can't be initialized.
|
||||
* ``keyring.errors.PasswordSetError``: Raise when password can't be set in the keyring.
|
||||
* ``keyring.errors.PasswordDeleteError``: Raised when the password can't be deleted in the keyring.
|
||||
|
||||
------------
|
||||
Get involved
|
||||
------------
|
||||
|
||||
Python keyring lib is an open community project and highly welcomes new
|
||||
contributors.
|
||||
|
||||
* Repository: https://github.com/jaraco/keyring/
|
||||
* Bug Tracker: https://github.com/jaraco/keyring/issues/
|
||||
* Mailing list: http://groups.google.com/group/python-keyring
|
||||
|
||||
Security Contact
|
||||
================
|
||||
|
||||
If you wish to report a security vulnerability, the public disclosure
|
||||
of which may exacerbate the risk, please
|
||||
`Contact Tidelift security <https://tidelift.com/security>`_,
|
||||
which will coordinate the fix and disclosure privately.
|
||||
|
||||
Making Releases
|
||||
===============
|
||||
|
||||
This project makes use of automated releases via Travis-CI. The
|
||||
simple workflow is to tag a commit and push it to Github. If it
|
||||
passes tests on a late Python version, it will be automatically
|
||||
deployed to PyPI.
|
||||
|
||||
Other things to consider when making a release:
|
||||
|
||||
- first ensure that tests pass (preferably on Windows and Linux)
|
||||
- check that the changelog is current for the intended release
|
||||
|
||||
Running Tests
|
||||
=============
|
||||
|
||||
Tests are `continuously run <https://travis-ci.org/#!/jaraco/keyring>`_ using
|
||||
Travis-CI.
|
||||
|
||||
To run the tests yourself, you'll want keyring installed to some environment
|
||||
in which it can be tested. Recommended technique is described below.
|
||||
|
||||
Using tox
|
||||
---------
|
||||
|
||||
Keyring prefers use of `tox <https://pypi.org/project/tox>`_ to run tests.
|
||||
Simply install and invoke ``tox``.
|
||||
|
||||
This technique is the one used by the Travis-CI script.
|
||||
|
||||
----------
|
||||
Background
|
||||
----------
|
||||
|
||||
The project was based on Tarek Ziade's idea in `this post`_. Kang Zhang
|
||||
initially carried it out as a `Google Summer of Code`_ project, and Tarek
|
||||
mentored Kang on this project.
|
||||
|
||||
.. _this post: http://tarekziade.wordpress.com/2009/03/27/pycon-hallway-session-1-a-keyring-library-for-python/
|
||||
.. _Google Summer of Code: http://socghop.appspot.com/
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/jaraco/keyring.svg
|
||||
:alt: Join the chat at https://gitter.im/jaraco/keyring
|
||||
:target: https://gitter.im/jaraco/keyring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Requires-Python: >=2.7
|
||||
Provides-Extra: docs
|
||||
Provides-Extra: testing
|
|
@ -0,0 +1,61 @@
|
|||
.flake8
|
||||
.gitignore
|
||||
.readthedocs.yml
|
||||
.travis-macos
|
||||
.travis.yml
|
||||
CHANGES.rst
|
||||
LICENSE
|
||||
README.rst
|
||||
appveyor.yml
|
||||
conftest.py
|
||||
hook-keyring.backend.py
|
||||
pyproject.toml
|
||||
pytest.ini
|
||||
setup.cfg
|
||||
setup.py
|
||||
skeleton.md
|
||||
tox.ini
|
||||
docs/conf.py
|
||||
docs/history.rst
|
||||
docs/index.rst
|
||||
docs/_templates/tidelift-sidebar.html
|
||||
keyring/__init__.py
|
||||
keyring/__main__.py
|
||||
keyring/backend.py
|
||||
keyring/cli.py
|
||||
keyring/core.py
|
||||
keyring/credentials.py
|
||||
keyring/devpi_client.py
|
||||
keyring/errors.py
|
||||
keyring/http.py
|
||||
keyring/py27compat.py
|
||||
keyring/py32compat.py
|
||||
keyring/py33compat.py
|
||||
keyring.egg-info/PKG-INFO
|
||||
keyring.egg-info/SOURCES.txt
|
||||
keyring.egg-info/dependency_links.txt
|
||||
keyring.egg-info/entry_points.txt
|
||||
keyring.egg-info/requires.txt
|
||||
keyring.egg-info/top_level.txt
|
||||
keyring/backends/OS_X.py
|
||||
keyring/backends/SecretService.py
|
||||
keyring/backends/Windows.py
|
||||
keyring/backends/_OS_X_API.py
|
||||
keyring/backends/__init__.py
|
||||
keyring/backends/chainer.py
|
||||
keyring/backends/fail.py
|
||||
keyring/backends/kwallet.py
|
||||
keyring/backends/null.py
|
||||
keyring/tests/__init__.py
|
||||
keyring/tests/test_backend.py
|
||||
keyring/tests/util.py
|
||||
keyring/tests/backends/__init__.py
|
||||
keyring/tests/backends/test_OS_X.py
|
||||
keyring/tests/backends/test_SecretService.py
|
||||
keyring/tests/backends/test_Windows.py
|
||||
keyring/tests/backends/test_chainer.py
|
||||
keyring/tests/backends/test_kwallet.py
|
||||
keyring/util/__init__.py
|
||||
keyring/util/platform_.py
|
||||
keyring/util/properties.py
|
||||
tests/test_packaging.py
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
[console_scripts]
|
||||
keyring = keyring.cli:main
|
||||
|
||||
[devpi_client]
|
||||
keyring = keyring.devpi_client
|
||||
|
||||
[keyring.backends]
|
||||
KWallet = keyring.backends.kwallet
|
||||
SecretService = keyring.backends.SecretService
|
||||
Windows = keyring.backends.Windows
|
||||
chainer = keyring.backends.chainer
|
||||
macOS = keyring.backends.OS_X
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
entrypoints
|
||||
|
||||
[:(sys_platform == "linux2" or sys_platform == "linux") and python_version < "3.5"]
|
||||
secretstorage<3
|
||||
|
||||
[:sys_platform == "linux" and python_version >= "3.5"]
|
||||
secretstorage
|
||||
|
||||
[:sys_platform == "win32"]
|
||||
pywin32-ctypes!=0.1.0,!=0.1.1
|
||||
|
||||
[docs]
|
||||
sphinx
|
||||
jaraco.packaging>=3.2
|
||||
rst.linker>=1.9
|
||||
|
||||
[testing]
|
||||
pytest!=3.7.3,>=3.5
|
||||
pytest-checkdocs
|
||||
pytest-flake8
|
|
@ -0,0 +1 @@
|
|||
keyring
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from .core import (
|
||||
set_keyring, get_keyring, set_password, get_password,
|
||||
delete_password, get_credential)
|
||||
|
||||
__all__ = (
|
||||
'set_keyring', 'get_keyring', 'set_password', 'get_password',
|
||||
'delete_password', 'get_credential',
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
if __name__ == '__main__':
|
||||
from keyring import cli
|
||||
cli.main()
|
|
@ -0,0 +1,210 @@
|
|||
"""
|
||||
Keyring implementation support
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import entrypoints
|
||||
|
||||
from . import credentials, errors, util
|
||||
from .util import properties
|
||||
from .py27compat import add_metaclass, filter
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
by_priority = operator.attrgetter('priority')
|
||||
_limit = None
|
||||
|
||||
|
||||
class KeyringBackendMeta(abc.ABCMeta):
|
||||
"""
|
||||
A metaclass that's both an ABCMeta and a type that keeps a registry of
|
||||
all (non-abstract) types.
|
||||
"""
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(KeyringBackendMeta, cls).__init__(name, bases, dict)
|
||||
if not hasattr(cls, '_classes'):
|
||||
cls._classes = set()
|
||||
classes = cls._classes
|
||||
if not cls.__abstractmethods__:
|
||||
classes.add(cls)
|
||||
|
||||
|
||||
@add_metaclass(KeyringBackendMeta)
|
||||
class KeyringBackend:
|
||||
"""The abstract base class of the keyring, every backend must implement
|
||||
this interface.
|
||||
"""
|
||||
|
||||
# @abc.abstractproperty
|
||||
def priority(cls):
|
||||
"""
|
||||
Each backend class must supply a priority, a number (float or integer)
|
||||
indicating the priority of the backend relative to all other backends.
|
||||
The priority need not be static -- it may (and should) vary based
|
||||
attributes of the environment in which is runs (platform, available
|
||||
packages, etc.).
|
||||
|
||||
A higher number indicates a higher priority. The priority should raise
|
||||
a RuntimeError with a message indicating the underlying cause if the
|
||||
backend is not suitable for the current environment.
|
||||
|
||||
As a rule of thumb, a priority between zero but less than one is
|
||||
suitable, but a priority of one or greater is recommended.
|
||||
"""
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def viable(cls):
|
||||
with errors.ExceptionRaisedContext() as exc:
|
||||
cls.priority
|
||||
return not bool(exc)
|
||||
|
||||
@classmethod
|
||||
def get_viable_backends(cls):
|
||||
"""
|
||||
Return all subclasses deemed viable.
|
||||
"""
|
||||
return filter(operator.attrgetter('viable'), cls._classes)
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def name(cls):
|
||||
"""
|
||||
The keyring name, suitable for display.
|
||||
|
||||
The name is derived from module and class name.
|
||||
"""
|
||||
parent, sep, mod_name = cls.__module__.rpartition('.')
|
||||
mod_name = mod_name.replace('_', ' ')
|
||||
return ' '.join([mod_name, cls.__name__])
|
||||
|
||||
def __str__(self):
|
||||
keyring_class = type(self)
|
||||
return ("%s.%s (priority: %g)" % (keyring_class.__module__,
|
||||
keyring_class.__name__,
|
||||
keyring_class.priority))
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_password(self, service, username):
|
||||
"""Get password of the username for the service
|
||||
"""
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_password(self, service, username, password):
|
||||
"""Set password for the username of the service.
|
||||
|
||||
If the backend cannot store passwords, raise
|
||||
NotImplementedError.
|
||||
"""
|
||||
raise errors.PasswordSetError("reason")
|
||||
|
||||
# for backward-compatibility, don't require a backend to implement
|
||||
# delete_password
|
||||
# @abc.abstractmethod
|
||||
def delete_password(self, service, username):
|
||||
"""Delete the password for the username of the service.
|
||||
|
||||
If the backend cannot store passwords, raise
|
||||
NotImplementedError.
|
||||
"""
|
||||
raise errors.PasswordDeleteError("reason")
|
||||
|
||||
# for backward-compatibility, don't require a backend to implement
|
||||
# get_credential
|
||||
# @abc.abstractmethod
|
||||
def get_credential(self, service, username):
|
||||
"""Gets the username and password for the service.
|
||||
Returns a Credential instance.
|
||||
|
||||
The *username* argument is optional and may be omitted by
|
||||
the caller or ignored by the backend. Callers must use the
|
||||
returned username.
|
||||
"""
|
||||
# The default implementation requires a username here.
|
||||
if username is not None:
|
||||
password = self.get_password(service, username)
|
||||
if password is not None:
|
||||
return credentials.SimpleCredential(
|
||||
username,
|
||||
password,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class Crypter:
|
||||
"""Base class providing encryption and decryption
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def encrypt(self, value):
|
||||
"""Encrypt the value.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decrypt(self, value):
|
||||
"""Decrypt the value.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NullCrypter(Crypter):
|
||||
"""A crypter that does nothing
|
||||
"""
|
||||
|
||||
def encrypt(self, value):
|
||||
return value
|
||||
|
||||
def decrypt(self, value):
|
||||
return value
|
||||
|
||||
|
||||
def _load_plugins():
|
||||
"""
|
||||
Locate all setuptools entry points by the name 'keyring backends'
|
||||
and initialize them.
|
||||
Any third-party library may register an entry point by adding the
|
||||
following to their setup.py::
|
||||
|
||||
entry_points = {
|
||||
'keyring.backends': [
|
||||
'plugin_name = mylib.mymodule:initialize_func',
|
||||
],
|
||||
},
|
||||
|
||||
`plugin_name` can be anything, and is only used to display the name
|
||||
of the plugin at initialization time.
|
||||
|
||||
`initialize_func` is optional, but will be invoked if callable.
|
||||
"""
|
||||
group = 'keyring.backends'
|
||||
entry_points = entrypoints.get_group_all(group=group)
|
||||
for ep in entry_points:
|
||||
try:
|
||||
log.info('Loading %s', ep.name)
|
||||
init_func = ep.load()
|
||||
if callable(init_func):
|
||||
init_func()
|
||||
except Exception:
|
||||
log.exception("Error initializing plugin %s." % ep)
|
||||
|
||||
|
||||
@util.once
|
||||
def get_all_keyring():
|
||||
"""
|
||||
Return a list of all implemented keyrings that can be constructed without
|
||||
parameters.
|
||||
"""
|
||||
_load_plugins()
|
||||
viable_classes = KeyringBackend.get_viable_backends()
|
||||
rings = util.suppress_exceptions(viable_classes, exceptions=TypeError)
|
||||
return list(rings)
|
|
@ -0,0 +1,70 @@
|
|||
import platform
|
||||
|
||||
from ..backend import KeyringBackend
|
||||
from ..errors import PasswordSetError
|
||||
from ..errors import PasswordDeleteError
|
||||
from ..errors import KeyringLocked
|
||||
from ..errors import KeyringError
|
||||
from ..util import properties
|
||||
|
||||
try:
|
||||
from . import _OS_X_API as api
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""macOS Keychain"""
|
||||
|
||||
keychain = None
|
||||
"Pathname to keychain filename, overriding default keychain."
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
"""
|
||||
Preferred for all macOS environments.
|
||||
"""
|
||||
if platform.system() != 'Darwin':
|
||||
raise RuntimeError("macOS required")
|
||||
return 5
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
api.set_generic_password(
|
||||
self.keychain, service, username, password)
|
||||
except api.KeychainDenied as e:
|
||||
raise KeyringLocked("Can't store password on keychain: "
|
||||
"{}".format(e))
|
||||
except api.Error as e:
|
||||
raise PasswordSetError("Can't store password on keychain: "
|
||||
"{}".format(e))
|
||||
|
||||
def get_password(self, service, username):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
return api.find_generic_password(self.keychain, service, username)
|
||||
except api.NotFound:
|
||||
pass
|
||||
except api.KeychainDenied as e:
|
||||
raise KeyringLocked("Can't get password from keychain: "
|
||||
"{}".format(e))
|
||||
except api.Error as e:
|
||||
raise KeyringError("Can't get password from keychain: "
|
||||
"{}".format(e))
|
||||
|
||||
def delete_password(self, service, username):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
return api.delete_generic_password(
|
||||
self.keychain, service, username)
|
||||
except api.Error as e:
|
||||
raise PasswordDeleteError("Can't delete password in keychain: "
|
||||
"{}".format(e))
|
|
@ -0,0 +1,94 @@
|
|||
import logging
|
||||
|
||||
from ..util import properties
|
||||
from ..backend import KeyringBackend
|
||||
from ..errors import (InitError, PasswordDeleteError,
|
||||
ExceptionRaisedContext, KeyringLocked)
|
||||
|
||||
try:
|
||||
import secretstorage
|
||||
import secretstorage.exceptions as exceptions
|
||||
except ImportError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# See https://github.com/jaraco/keyring/issues/296
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""Secret Service Keyring"""
|
||||
appid = 'Python keyring library'
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
with ExceptionRaisedContext() as exc:
|
||||
secretstorage.__name__
|
||||
if exc:
|
||||
raise RuntimeError("SecretStorage required")
|
||||
if not hasattr(secretstorage, 'get_default_collection'):
|
||||
raise RuntimeError("SecretStorage 1.0 or newer required")
|
||||
try:
|
||||
bus = secretstorage.dbus_init()
|
||||
list(secretstorage.get_all_collections(bus))
|
||||
except exceptions.SecretStorageException as e:
|
||||
raise RuntimeError(
|
||||
"Unable to initialize SecretService: %s" % e)
|
||||
return 5
|
||||
|
||||
def get_preferred_collection(self):
|
||||
"""If self.preferred_collection contains a D-Bus path,
|
||||
the collection at that address is returned. Otherwise,
|
||||
the default collection is returned.
|
||||
"""
|
||||
bus = secretstorage.dbus_init()
|
||||
try:
|
||||
if hasattr(self, 'preferred_collection'):
|
||||
collection = secretstorage.Collection(
|
||||
bus, self.preferred_collection)
|
||||
else:
|
||||
collection = secretstorage.get_default_collection(bus)
|
||||
except exceptions.SecretStorageException as e:
|
||||
raise InitError("Failed to create the collection: %s." % e)
|
||||
if collection.is_locked():
|
||||
collection.unlock()
|
||||
if collection.is_locked(): # User dismissed the prompt
|
||||
raise KeyringLocked("Failed to unlock the collection!")
|
||||
return collection
|
||||
|
||||
def get_password(self, service, username):
|
||||
"""Get password of the username for the service
|
||||
"""
|
||||
collection = self.get_preferred_collection()
|
||||
items = collection.search_items(
|
||||
{"username": username, "service": service})
|
||||
for item in items:
|
||||
if hasattr(item, 'unlock'):
|
||||
item.unlock()
|
||||
if item.is_locked(): # User dismissed the prompt
|
||||
raise KeyringLocked('Failed to unlock the item!')
|
||||
return item.get_secret().decode('utf-8')
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
"""Set password for the username of the service
|
||||
"""
|
||||
collection = self.get_preferred_collection()
|
||||
attributes = {
|
||||
"application": self.appid,
|
||||
"service": service,
|
||||
"username": username
|
||||
}
|
||||
label = "Password for '%s' on '%s'" % (username, service)
|
||||
collection.create_item(label, attributes, password, replace=True)
|
||||
|
||||
def delete_password(self, service, username):
|
||||
"""Delete the stored password (only the first one)
|
||||
"""
|
||||
collection = self.get_preferred_collection()
|
||||
items = collection.search_items(
|
||||
{"username": username, "service": service})
|
||||
for item in items:
|
||||
return item.delete()
|
||||
raise PasswordDeleteError("No such password!")
|
|
@ -0,0 +1,165 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
|
||||
from ..py27compat import text_type
|
||||
from ..util import properties
|
||||
from ..backend import KeyringBackend
|
||||
from ..credentials import SimpleCredential
|
||||
from ..errors import PasswordDeleteError, ExceptionRaisedContext
|
||||
|
||||
|
||||
with ExceptionRaisedContext() as missing_deps:
|
||||
try:
|
||||
# prefer pywin32-ctypes
|
||||
from win32ctypes.pywin32 import pywintypes
|
||||
from win32ctypes.pywin32 import win32cred
|
||||
# force demand import to raise ImportError
|
||||
win32cred.__name__
|
||||
except ImportError:
|
||||
# fallback to pywin32
|
||||
import pywintypes
|
||||
import win32cred
|
||||
# force demand import to raise ImportError
|
||||
win32cred.__name__
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class WinVaultKeyring(KeyringBackend):
|
||||
"""
|
||||
WinVaultKeyring stores encrypted passwords using the Windows Credential
|
||||
Manager.
|
||||
|
||||
Requires pywin32
|
||||
|
||||
This backend does some gymnastics to simulate multi-user support,
|
||||
which WinVault doesn't support natively. See
|
||||
https://bitbucket.org/kang/python-keyring-lib/issue/47/winvaultkeyring-only-ever-returns-last#comment-731977
|
||||
for details on the implementation, but here's the gist:
|
||||
|
||||
Passwords are stored under the service name unless there is a collision
|
||||
(another password with the same service name but different user name),
|
||||
in which case the previous password is moved into a compound name:
|
||||
{username}@{service}
|
||||
"""
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
"""
|
||||
If available, the preferred backend on Windows.
|
||||
"""
|
||||
if missing_deps:
|
||||
raise RuntimeError("Requires Windows and pywin32")
|
||||
return 5
|
||||
|
||||
@staticmethod
|
||||
def _compound_name(username, service):
|
||||
return '%(username)s@%(service)s' % vars()
|
||||
|
||||
def get_password(self, service, username):
|
||||
# first attempt to get the password under the service name
|
||||
res = self._get_password(service)
|
||||
if not res or res['UserName'] != username:
|
||||
# It wasn't found so attempt to get it with the compound name
|
||||
res = self._get_password(self._compound_name(username, service))
|
||||
if not res:
|
||||
return None
|
||||
blob = res['CredentialBlob']
|
||||
return blob.decode('utf-16')
|
||||
|
||||
def _get_password(self, target):
|
||||
try:
|
||||
res = win32cred.CredRead(
|
||||
Type=win32cred.CRED_TYPE_GENERIC,
|
||||
TargetName=target,
|
||||
)
|
||||
except pywintypes.error as e:
|
||||
e = OldPywinError.wrap(e)
|
||||
if e.winerror == 1168 and e.funcname == 'CredRead': # not found
|
||||
return None
|
||||
raise
|
||||
return res
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
existing_pw = self._get_password(service)
|
||||
if existing_pw:
|
||||
# resave the existing password using a compound target
|
||||
existing_username = existing_pw['UserName']
|
||||
target = self._compound_name(existing_username, service)
|
||||
self._set_password(target, existing_username,
|
||||
existing_pw['CredentialBlob'].decode('utf-16'))
|
||||
self._set_password(service, username, text_type(password))
|
||||
|
||||
def _set_password(self, target, username, password):
|
||||
credential = dict(Type=win32cred.CRED_TYPE_GENERIC,
|
||||
TargetName=target,
|
||||
UserName=username,
|
||||
CredentialBlob=password,
|
||||
Comment="Stored using python-keyring",
|
||||
Persist=win32cred.CRED_PERSIST_ENTERPRISE)
|
||||
win32cred.CredWrite(credential, 0)
|
||||
|
||||
def delete_password(self, service, username):
|
||||
compound = self._compound_name(username, service)
|
||||
deleted = False
|
||||
for target in service, compound:
|
||||
existing_pw = self._get_password(target)
|
||||
if existing_pw and existing_pw['UserName'] == username:
|
||||
deleted = True
|
||||
self._delete_password(target)
|
||||
if not deleted:
|
||||
raise PasswordDeleteError(service)
|
||||
|
||||
def _delete_password(self, target):
|
||||
try:
|
||||
win32cred.CredDelete(
|
||||
Type=win32cred.CRED_TYPE_GENERIC,
|
||||
TargetName=target,
|
||||
)
|
||||
except pywintypes.error as e:
|
||||
e = OldPywinError.wrap(e)
|
||||
if e.winerror == 1168 and e.funcname == 'CredDelete': # not found
|
||||
return
|
||||
raise
|
||||
|
||||
def get_credential(self, service, username):
|
||||
res = None
|
||||
# get the credentials associated with the provided username
|
||||
if username:
|
||||
res = self._get_password(self._compound_name(username, service))
|
||||
# get any first password under the service name
|
||||
if not res:
|
||||
res = self._get_password(service)
|
||||
if not res:
|
||||
return None
|
||||
return SimpleCredential(
|
||||
res['UserName'],
|
||||
res['CredentialBlob'].decode('utf-16'),
|
||||
)
|
||||
|
||||
|
||||
class OldPywinError:
|
||||
"""
|
||||
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
|
|
@ -0,0 +1,345 @@
|
|||
import contextlib
|
||||
import ctypes
|
||||
import struct
|
||||
from ctypes import c_void_p, c_uint16, c_uint32, c_int32, c_char_p, POINTER
|
||||
|
||||
from keyring.py27compat import string_types, add_metaclass
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
sec_keychain_ref = sec_keychain_item_ref = c_void_p
|
||||
OS_status = c_int32
|
||||
|
||||
|
||||
class error:
|
||||
item_not_found = -25300
|
||||
keychain_denied = -128
|
||||
sec_auth_failed = -25293
|
||||
plist_missing = -67030
|
||||
|
||||
|
||||
fw = '/System/Library/Frameworks/{name}.framework/Versions/A/{name}'.format
|
||||
_sec = ctypes.CDLL(fw(name='Security'))
|
||||
_core = ctypes.CDLL(fw(name='CoreServices'))
|
||||
|
||||
|
||||
SecKeychainOpen = _sec.SecKeychainOpen
|
||||
SecKeychainOpen.argtypes = (
|
||||
c_char_p,
|
||||
POINTER(sec_keychain_ref),
|
||||
)
|
||||
SecKeychainOpen.restype = OS_status
|
||||
|
||||
|
||||
SecKeychainCopyDefault = _sec.SecKeychainCopyDefault
|
||||
SecKeychainCopyDefault.argtypes = POINTER(sec_keychain_ref),
|
||||
SecKeychainCopyDefault.restype = OS_status
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@classmethod
|
||||
def raise_for_status(cls, status):
|
||||
if status == 0:
|
||||
return
|
||||
if status == error.item_not_found:
|
||||
raise NotFound(status, "Item not found")
|
||||
if status == error.keychain_denied:
|
||||
raise KeychainDenied(status, "Keychain Access Denied")
|
||||
if status == error.sec_auth_failed or status == error.plist_missing:
|
||||
raise SecAuthFailure(status, "Security Auth Failure: make sure "
|
||||
"python is signed with codesign util")
|
||||
raise cls(status, "Unknown Error")
|
||||
|
||||
|
||||
class NotFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class KeychainDenied(Error):
|
||||
pass
|
||||
|
||||
|
||||
class SecAuthFailure(Error):
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open(name):
|
||||
ref = sec_keychain_ref()
|
||||
if name is None:
|
||||
status = SecKeychainCopyDefault(ref)
|
||||
else:
|
||||
status = SecKeychainOpen(name.encode('utf-8'), ref)
|
||||
Error.raise_for_status(status)
|
||||
try:
|
||||
yield ref
|
||||
finally:
|
||||
_core.CFRelease(ref)
|
||||
|
||||
|
||||
SecKeychainFindGenericPassword = _sec.SecKeychainFindGenericPassword
|
||||
SecKeychainFindGenericPassword.argtypes = (
|
||||
sec_keychain_ref,
|
||||
c_uint32,
|
||||
c_char_p,
|
||||
c_uint32,
|
||||
c_char_p,
|
||||
POINTER(c_uint32), # passwordLength
|
||||
POINTER(c_void_p), # passwordData
|
||||
POINTER(sec_keychain_item_ref), # itemRef
|
||||
)
|
||||
SecKeychainFindGenericPassword.restype = OS_status
|
||||
|
||||
|
||||
def find_generic_password(kc_name, service, username):
|
||||
username = username.encode('utf-8')
|
||||
service = service.encode('utf-8')
|
||||
with open(kc_name) as keychain:
|
||||
length = c_uint32()
|
||||
data = c_void_p()
|
||||
status = SecKeychainFindGenericPassword(
|
||||
keychain,
|
||||
len(service),
|
||||
service,
|
||||
len(username),
|
||||
username,
|
||||
length,
|
||||
data,
|
||||
None,
|
||||
)
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
password = ctypes.create_string_buffer(length.value)
|
||||
ctypes.memmove(password, data.value, length.value)
|
||||
SecKeychainItemFreeContent(None, data)
|
||||
return password.raw.decode('utf-8')
|
||||
|
||||
|
||||
SecKeychainFindInternetPassword = _sec.SecKeychainFindInternetPassword
|
||||
SecKeychainFindInternetPassword.argtypes = (
|
||||
sec_keychain_ref, # keychainOrArray
|
||||
c_uint32, # serverNameLength
|
||||
c_char_p, # serverName
|
||||
c_uint32, # securityDomainLength
|
||||
c_char_p, # securityDomain
|
||||
c_uint32, # accountNameLength
|
||||
c_char_p, # accountName
|
||||
c_uint32, # pathLength
|
||||
c_char_p, # path
|
||||
c_uint16, # port
|
||||
c_uint32, # SecProtocolType protocol,
|
||||
c_uint32, # SecAuthenticationType authenticationType,
|
||||
POINTER(c_uint32), # passwordLength
|
||||
POINTER(c_void_p), # passwordData
|
||||
POINTER(sec_keychain_item_ref), # itemRef
|
||||
)
|
||||
SecKeychainFindInternetPassword.restype = OS_status
|
||||
|
||||
|
||||
class PackedAttributes(type):
|
||||
"""
|
||||
Take the attributes which use magic words
|
||||
to represent enumerated constants and generate
|
||||
the constants.
|
||||
"""
|
||||
def __new__(cls, name, bases, dict):
|
||||
dict.update(
|
||||
(key, cls.unpack(val))
|
||||
for key, val in dict.items()
|
||||
if not key.startswith('_')
|
||||
)
|
||||
return super(PackedAttributes, cls).__new__(cls, name, bases, dict)
|
||||
|
||||
@staticmethod
|
||||
def unpack(word):
|
||||
r"""
|
||||
>>> PackedAttributes.unpack(0)
|
||||
0
|
||||
>>> PackedAttributes.unpack('\x00\x00\x00\x01')
|
||||
1
|
||||
>>> PackedAttributes.unpack('abcd')
|
||||
1633837924
|
||||
"""
|
||||
if not isinstance(word, string_types):
|
||||
return word
|
||||
val, = struct.unpack('!I', word.encode('ascii'))
|
||||
return val
|
||||
|
||||
|
||||
@add_metaclass(PackedAttributes)
|
||||
class SecProtocolType:
|
||||
kSecProtocolTypeHTTP = 'http'
|
||||
kSecProtocolTypeHTTPS = 'htps'
|
||||
kSecProtocolTypeFTP = 'ftp '
|
||||
|
||||
|
||||
@add_metaclass(PackedAttributes)
|
||||
class SecAuthenticationType:
|
||||
"""
|
||||
>>> SecAuthenticationType.kSecAuthenticationTypeDefault
|
||||
1684434036
|
||||
"""
|
||||
kSecAuthenticationTypeDefault = 'dflt'
|
||||
kSecAuthenticationTypeAny = 0
|
||||
|
||||
|
||||
def find_internet_password(kc_name, service, username):
|
||||
username = username.encode('utf-8')
|
||||
domain = None
|
||||
service = service.encode('utf-8')
|
||||
path = None
|
||||
port = 0
|
||||
|
||||
with open(kc_name) as keychain:
|
||||
length = c_uint32()
|
||||
data = c_void_p()
|
||||
status = SecKeychainFindInternetPassword(
|
||||
keychain,
|
||||
len(service), service,
|
||||
0, domain,
|
||||
len(username), username,
|
||||
0, path,
|
||||
port,
|
||||
SecProtocolType.kSecProtocolTypeHTTPS,
|
||||
SecAuthenticationType.kSecAuthenticationTypeAny,
|
||||
length,
|
||||
data,
|
||||
None,
|
||||
)
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
password = ctypes.create_string_buffer(length.value)
|
||||
ctypes.memmove(password, data.value, length.value)
|
||||
SecKeychainItemFreeContent(None, data)
|
||||
return password.raw.decode('utf-8')
|
||||
|
||||
|
||||
SecKeychainAddGenericPassword = _sec.SecKeychainAddGenericPassword
|
||||
SecKeychainAddGenericPassword.argtypes = (
|
||||
sec_keychain_ref,
|
||||
c_uint32,
|
||||
c_char_p,
|
||||
c_uint32,
|
||||
c_char_p,
|
||||
c_uint32,
|
||||
c_char_p,
|
||||
POINTER(sec_keychain_item_ref),
|
||||
)
|
||||
SecKeychainAddGenericPassword.restype = OS_status
|
||||
|
||||
|
||||
def set_generic_password(name, service, username, password):
|
||||
username = username.encode('utf-8')
|
||||
service = service.encode('utf-8')
|
||||
password = password.encode('utf-8')
|
||||
with open(name) as keychain:
|
||||
item = sec_keychain_item_ref()
|
||||
status = SecKeychainFindGenericPassword(
|
||||
keychain,
|
||||
len(service), service,
|
||||
len(username), username, None,
|
||||
None, item)
|
||||
if status:
|
||||
if status == error.item_not_found:
|
||||
status = SecKeychainAddGenericPassword(
|
||||
keychain,
|
||||
len(service), service,
|
||||
len(username), username,
|
||||
len(password), password, None)
|
||||
else:
|
||||
status = SecKeychainItemModifyAttributesAndData(
|
||||
item, None, len(password), password)
|
||||
_core.CFRelease(item)
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
|
||||
SecKeychainAddInternetPassword = _sec.SecKeychainAddInternetPassword
|
||||
SecKeychainAddInternetPassword.argtypes = (
|
||||
sec_keychain_ref, # keychainOrArray
|
||||
c_uint32, # serverNameLength
|
||||
c_char_p, # serverName
|
||||
c_uint32, # securityDomainLength
|
||||
c_char_p, # securityDomain
|
||||
c_uint32, # accountNameLength
|
||||
c_char_p, # accountName
|
||||
c_uint32, # pathLength
|
||||
c_char_p, # path
|
||||
c_uint16, # port
|
||||
c_uint32, # SecProtocolType protocol,
|
||||
c_uint32, # SecAuthenticationType authenticationType,
|
||||
c_uint32, # passwordLength
|
||||
c_void_p, # passwordData
|
||||
POINTER(sec_keychain_item_ref), # itemRef
|
||||
)
|
||||
SecKeychainAddInternetPassword.restype = OS_status
|
||||
|
||||
|
||||
def set_internet_password(name, service, username, password):
|
||||
username = username.encode('utf-8')
|
||||
domain = None
|
||||
service = service.encode('utf-8')
|
||||
password = password.encode('utf-8')
|
||||
path = None
|
||||
port = 0
|
||||
with open(name) as keychain:
|
||||
# TODO: Use update or set technique as seen in set_generic_password
|
||||
status = SecKeychainAddInternetPassword(
|
||||
keychain,
|
||||
len(service), service,
|
||||
0, domain,
|
||||
len(username), username,
|
||||
0, path,
|
||||
port,
|
||||
SecProtocolType.kSecProtocolTypeHTTPS,
|
||||
SecAuthenticationType.kSecAuthenticationTypeAny,
|
||||
len(password), password,
|
||||
None,
|
||||
)
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
|
||||
SecKeychainItemModifyAttributesAndData = (
|
||||
_sec.SecKeychainItemModifyAttributesAndData)
|
||||
SecKeychainItemModifyAttributesAndData.argtypes = (
|
||||
sec_keychain_item_ref, c_void_p, c_uint32, c_void_p,
|
||||
)
|
||||
SecKeychainItemModifyAttributesAndData.restype = OS_status
|
||||
|
||||
SecKeychainItemFreeContent = _sec.SecKeychainItemFreeContent
|
||||
SecKeychainItemFreeContent.argtypes = (
|
||||
c_void_p, c_void_p,
|
||||
)
|
||||
SecKeychainItemFreeContent.restype = OS_status
|
||||
|
||||
SecKeychainItemDelete = _sec.SecKeychainItemDelete
|
||||
SecKeychainItemDelete.argtypes = sec_keychain_item_ref,
|
||||
SecKeychainItemDelete.restype = OS_status
|
||||
|
||||
|
||||
def delete_generic_password(name, service, username):
|
||||
username = username.encode('utf-8')
|
||||
service = service.encode('utf-8')
|
||||
with open(name) as keychain:
|
||||
length = c_uint32()
|
||||
data = c_void_p()
|
||||
item = sec_keychain_item_ref()
|
||||
status = SecKeychainFindGenericPassword(
|
||||
keychain,
|
||||
len(service),
|
||||
service,
|
||||
len(username),
|
||||
username,
|
||||
length,
|
||||
data,
|
||||
item,
|
||||
)
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
SecKeychainItemDelete(item)
|
||||
_core.CFRelease(item)
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Keyring Chainer - iterates over other viable backends to
|
||||
discover passwords in each.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .. import backend
|
||||
from ..util import properties
|
||||
|
||||
|
||||
class ChainerBackend(backend.KeyringBackend):
|
||||
"""
|
||||
>>> ChainerBackend()
|
||||
<keyring.backends.chainer.ChainerBackend object at ...>
|
||||
"""
|
||||
|
||||
# override viability as 'priority' cannot be determined
|
||||
# until other backends have been constructed
|
||||
viable = True
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
"""
|
||||
High-priority if there are backends to chain, otherwise 0.
|
||||
"""
|
||||
return 10 * (len(cls.backends) > 1)
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def backends(cls):
|
||||
"""
|
||||
Discover all keyrings for chaining.
|
||||
"""
|
||||
allowed = (
|
||||
keyring
|
||||
for keyring in filter(backend._limit, backend.get_all_keyring())
|
||||
if not isinstance(keyring, ChainerBackend)
|
||||
and keyring.priority > 0
|
||||
)
|
||||
return sorted(allowed, key=backend.by_priority, reverse=True)
|
||||
|
||||
def get_password(self, service, username):
|
||||
for keyring in self.backends:
|
||||
password = keyring.get_password(service, username)
|
||||
if password is not None:
|
||||
return password
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
for keyring in self.backends:
|
||||
try:
|
||||
return keyring.set_password(service, username, password)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def delete_password(self, service, username):
|
||||
for keyring in self.backends:
|
||||
try:
|
||||
return keyring.delete_password(service, username)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def get_credential(self, service, username):
|
||||
for keyring in self.backends:
|
||||
credential = keyring.get_credential(service, username)
|
||||
if credential is not None:
|
||||
return credential
|
|
@ -0,0 +1,26 @@
|
|||
from ..backend import KeyringBackend
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""
|
||||
Keyring that raises error on every operation.
|
||||
|
||||
>>> kr = Keyring()
|
||||
>>> kr.get_password('svc', 'user')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
RuntimeError: ...No recommended backend...
|
||||
"""
|
||||
|
||||
priority = 0
|
||||
|
||||
def get_password(self, service, username, password=None):
|
||||
msg = (
|
||||
"No recommended backend was available. Install a recommended 3rd "
|
||||
"party backend package; or, install the keyrings.alt package if "
|
||||
"you want to use the non-recommended backends. See "
|
||||
"https://pypi.org/project/keyring for details."
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
set_password = delete_password = get_password
|
|
@ -0,0 +1,140 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from ..backend import KeyringBackend
|
||||
from ..errors import PasswordDeleteError
|
||||
from ..errors import PasswordSetError, InitError, KeyringLocked
|
||||
from ..util import properties
|
||||
|
||||
try:
|
||||
import dbus
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
except ImportError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# See https://github.com/jaraco/keyring/issues/296
|
||||
pass
|
||||
|
||||
|
||||
class DBusKeyring(KeyringBackend):
|
||||
"""
|
||||
KDE KWallet 5 via D-Bus
|
||||
"""
|
||||
|
||||
appid = os.path.basename(sys.argv[0]) or 'Python keyring library'
|
||||
wallet = None
|
||||
bus_name = 'org.kde.kwalletd5'
|
||||
object_path = '/modules/kwalletd5'
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
if 'dbus' not in globals():
|
||||
raise RuntimeError('python-dbus not installed')
|
||||
try:
|
||||
bus = dbus.SessionBus(mainloop=DBusGMainLoop())
|
||||
except dbus.DBusException as exc:
|
||||
raise RuntimeError(exc.get_dbus_message())
|
||||
try:
|
||||
bus.get_object(cls.bus_name, cls.object_path)
|
||||
except dbus.DBusException:
|
||||
tmpl = 'cannot connect to {bus_name}'
|
||||
msg = tmpl.format(bus_name=cls.bus_name)
|
||||
raise RuntimeError(msg)
|
||||
if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":"):
|
||||
return 5.1
|
||||
return 4.9
|
||||
|
||||
def __init__(self, *arg, **kw):
|
||||
super(DBusKeyring, self).__init__(*arg, **kw)
|
||||
self.handle = -1
|
||||
|
||||
def _migrate(self, service):
|
||||
old_folder = 'Python'
|
||||
entry_list = []
|
||||
if self.iface.hasFolder(self.handle, old_folder, self.appid):
|
||||
entry_list = self.iface.readPasswordList(
|
||||
self.handle, old_folder, '*@*', self.appid)
|
||||
|
||||
for entry in entry_list.items():
|
||||
key = entry[0]
|
||||
password = entry[1]
|
||||
|
||||
username, service = key.rsplit('@', 1)
|
||||
ret = self.iface.writePassword(
|
||||
self.handle, service, username, password, self.appid)
|
||||
if ret == 0:
|
||||
self.iface.removeEntry(
|
||||
self.handle, old_folder, key, self.appid)
|
||||
|
||||
entry_list = self.iface.readPasswordList(
|
||||
self.handle, old_folder, '*', self.appid)
|
||||
if not entry_list:
|
||||
self.iface.removeFolder(self.handle, old_folder, self.appid)
|
||||
|
||||
def connected(self, service):
|
||||
if self.handle >= 0:
|
||||
if self.iface.isOpen(self.handle):
|
||||
return True
|
||||
|
||||
bus = dbus.SessionBus(mainloop=DBusGMainLoop())
|
||||
wId = 0
|
||||
try:
|
||||
remote_obj = bus.get_object(self.bus_name, self.object_path)
|
||||
self.iface = dbus.Interface(remote_obj, 'org.kde.KWallet')
|
||||
self.handle = self.iface.open(
|
||||
self.iface.networkWallet(), wId, self.appid)
|
||||
except dbus.DBusException as e:
|
||||
raise InitError('Failed to open keyring: %s.' % e)
|
||||
|
||||
if self.handle < 0:
|
||||
return False
|
||||
self._migrate(service)
|
||||
return True
|
||||
|
||||
def get_password(self, service, username):
|
||||
"""Get password of the username for the service
|
||||
"""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise KeyringLocked("Failed to unlock the keyring!")
|
||||
if not self.iface.hasEntry(self.handle, service, username, self.appid):
|
||||
return None
|
||||
password = self.iface.readPassword(
|
||||
self.handle, service, username, self.appid)
|
||||
return str(password)
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
"""Set password for the username of the service
|
||||
"""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise PasswordSetError("Cancelled by user")
|
||||
self.iface.writePassword(
|
||||
self.handle, service, username, password, self.appid)
|
||||
|
||||
def delete_password(self, service, username):
|
||||
"""Delete the password for the username of the service.
|
||||
"""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise PasswordDeleteError("Cancelled by user")
|
||||
if not self.iface.hasEntry(self.handle, service, username, self.appid):
|
||||
raise PasswordDeleteError("Password not found")
|
||||
self.iface.removeEntry(self.handle, service, username, self.appid)
|
||||
|
||||
|
||||
class DBusKeyringKWallet4(DBusKeyring):
|
||||
"""
|
||||
KDE KWallet 4 via D-Bus
|
||||
"""
|
||||
|
||||
bus_name = 'org.kde.kwalletd'
|
||||
object_path = '/modules/kwalletd'
|
||||
|
||||
@properties.ClassProperty
|
||||
@classmethod
|
||||
def priority(cls):
|
||||
return super(DBusKeyringKWallet4, cls).priority - 1
|
|
@ -0,0 +1,17 @@
|
|||
from ..backend import KeyringBackend
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""
|
||||
Keyring that return None on every operation.
|
||||
|
||||
>>> kr = Keyring()
|
||||
>>> kr.get_password('svc', 'user')
|
||||
"""
|
||||
|
||||
priority = -1
|
||||
|
||||
def get_password(self, service, username, password=None):
|
||||
pass
|
||||
|
||||
set_password = delete_password = get_password
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python
|
||||
"""Simple command line interface to get/set password from a keyring"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
import sys
|
||||
|
||||
from . import core
|
||||
from . import backend
|
||||
from . import set_keyring, get_password, set_password, delete_password
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class CommandLineTool:
|
||||
def __init__(self):
|
||||
self.parser = OptionParser(
|
||||
usage="%prog [get|set|del] SERVICE USERNAME",
|
||||
)
|
||||
self.parser.add_option("-p", "--keyring-path",
|
||||
dest="keyring_path", default=None,
|
||||
help="Path to the keyring backend")
|
||||
self.parser.add_option("-b", "--keyring-backend",
|
||||
dest="keyring_backend", default=None,
|
||||
help="Name of the keyring backend")
|
||||
self.parser.add_option("--list-backends",
|
||||
action="store_true",
|
||||
help="List keyring backends and exit")
|
||||
self.parser.add_option("--disable",
|
||||
action="store_true",
|
||||
help="Disable keyring and exit")
|
||||
|
||||
def run(self, argv):
|
||||
opts, args = self.parser.parse_args(argv)
|
||||
|
||||
if opts.list_backends:
|
||||
for k in backend.get_all_keyring():
|
||||
print(k)
|
||||
return
|
||||
|
||||
if opts.disable:
|
||||
core.disable()
|
||||
return
|
||||
|
||||
try:
|
||||
kind, service, username = args
|
||||
except ValueError:
|
||||
if len(args) == 0:
|
||||
# Be nice with the user if he just tries to launch the tool
|
||||
self.parser.print_help()
|
||||
return 1
|
||||
else:
|
||||
self.parser.error("Wrong number of arguments")
|
||||
|
||||
if opts.keyring_backend is not None:
|
||||
try:
|
||||
if opts.keyring_path:
|
||||
sys.path.insert(0, opts.keyring_path)
|
||||
set_keyring(core.load_keyring(opts.keyring_backend))
|
||||
except (Exception,):
|
||||
# Tons of things can go wrong here:
|
||||
# ImportError when using "fjkljfljkl"
|
||||
# AttributeError when using "os.path.bar"
|
||||
# TypeError when using "__builtins__.str"
|
||||
# So, we play on the safe side, and catch everything.
|
||||
e = sys.exc_info()[1]
|
||||
self.parser.error("Unable to load specified keyring: %s" % e)
|
||||
|
||||
if kind == 'get':
|
||||
password = get_password(service, username)
|
||||
if password is None:
|
||||
return 1
|
||||
|
||||
self.output_password(password)
|
||||
return 0
|
||||
|
||||
elif kind == 'set':
|
||||
password = self.input_password("Password for '%s' in '%s': " %
|
||||
(username, service))
|
||||
set_password(service, username, password)
|
||||
return 0
|
||||
|
||||
elif kind == 'del':
|
||||
password = self.input_password(
|
||||
"Deleting password for '%s' in '%s': " %
|
||||
(username, service),
|
||||
)
|
||||
delete_password(service, username)
|
||||
return 0
|
||||
|
||||
else:
|
||||
self.parser.error("You can only 'get', 'del' or 'set' a password.")
|
||||
pass
|
||||
|
||||
def input_password(self, prompt):
|
||||
"""Retrieve password from input.
|
||||
"""
|
||||
return self.pass_from_pipe() or getpass.getpass(prompt)
|
||||
|
||||
@classmethod
|
||||
def pass_from_pipe(cls):
|
||||
"""Return password from pipe if not on TTY, else False.
|
||||
"""
|
||||
is_pipe = not sys.stdin.isatty()
|
||||
return is_pipe and cls.strip_last_newline(sys.stdin.read())
|
||||
|
||||
@staticmethod
|
||||
def strip_last_newline(str):
|
||||
"""Strip one last newline, if present."""
|
||||
return str[:-str.endswith('\n')]
|
||||
|
||||
def output_password(self, password):
|
||||
"""Output the password to the user.
|
||||
|
||||
This mostly exists to ease the testing process.
|
||||
"""
|
||||
|
||||
print(password, file=sys.stdout)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""Main command line interface."""
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
cli = CommandLineTool()
|
||||
return cli.run(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
|
@ -0,0 +1,189 @@
|
|||
"""
|
||||
Core API functions and initialization routines.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from .py27compat import configparser, filter
|
||||
from .py33compat import max
|
||||
|
||||
from . import backend
|
||||
from .util import platform_ as platform
|
||||
from .backends import fail
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_keyring_backend = None
|
||||
|
||||
|
||||
def set_keyring(keyring):
|
||||
"""Set current keyring backend.
|
||||
"""
|
||||
global _keyring_backend
|
||||
if not isinstance(keyring, backend.KeyringBackend):
|
||||
raise TypeError("The keyring must be a subclass of KeyringBackend")
|
||||
_keyring_backend = keyring
|
||||
|
||||
|
||||
def get_keyring():
|
||||
"""Get current keyring backend.
|
||||
"""
|
||||
return _keyring_backend
|
||||
|
||||
|
||||
def disable():
|
||||
"""
|
||||
Configure the null keyring as the default.
|
||||
"""
|
||||
root = platform.config_root()
|
||||
try:
|
||||
os.makedirs(root)
|
||||
except OSError:
|
||||
pass
|
||||
filename = os.path.join(root, 'keyringrc.cfg')
|
||||
if os.path.exists(filename):
|
||||
msg = "Refusing to overwrite {filename}".format(**locals())
|
||||
raise RuntimeError(msg)
|
||||
with open(filename, 'w') as file:
|
||||
file.write('[backend]\ndefault-keyring=keyring.backends.null.Keyring')
|
||||
|
||||
|
||||
def get_password(service_name, username):
|
||||
"""Get password from the specified service.
|
||||
"""
|
||||
return _keyring_backend.get_password(service_name, username)
|
||||
|
||||
|
||||
def set_password(service_name, username, password):
|
||||
"""Set password for the user in the specified service.
|
||||
"""
|
||||
_keyring_backend.set_password(service_name, username, password)
|
||||
|
||||
|
||||
def delete_password(service_name, username):
|
||||
"""Delete the password for the user in the specified service.
|
||||
"""
|
||||
_keyring_backend.delete_password(service_name, username)
|
||||
|
||||
|
||||
def get_credential(service_name, username):
|
||||
"""Get a Credential for the specified service.
|
||||
"""
|
||||
return _keyring_backend.get_credential(service_name, username)
|
||||
|
||||
|
||||
def recommended(backend):
|
||||
return backend.priority >= 1
|
||||
|
||||
|
||||
def init_backend(limit=None):
|
||||
"""
|
||||
Load a keyring specified in the config file or infer the best available.
|
||||
|
||||
Limit, if supplied, should be a callable taking a backend and returning
|
||||
True if that backend should be included for consideration.
|
||||
"""
|
||||
# save the limit for the chainer to honor
|
||||
backend._limit = limit
|
||||
|
||||
# get all keyrings passing the limit filter
|
||||
keyrings = filter(limit, backend.get_all_keyring())
|
||||
|
||||
set_keyring(
|
||||
load_env()
|
||||
or load_config()
|
||||
or max(keyrings, default=fail.Keyring(), key=backend.by_priority)
|
||||
)
|
||||
|
||||
|
||||
def _load_keyring_class(keyring_name):
|
||||
"""
|
||||
Load the keyring class indicated by name.
|
||||
|
||||
These popular names are tested to ensure their presence.
|
||||
|
||||
>>> popular_names = [
|
||||
... 'keyring.backends.Windows.WinVaultKeyring',
|
||||
... 'keyring.backends.OS_X.Keyring',
|
||||
... 'keyring.backends.kwallet.DBusKeyring',
|
||||
... 'keyring.backends.SecretService.Keyring',
|
||||
... ]
|
||||
>>> list(map(_load_keyring_class, popular_names))
|
||||
[...]
|
||||
|
||||
These legacy names are retained for compatibility.
|
||||
|
||||
>>> legacy_names = [
|
||||
... ]
|
||||
>>> list(map(_load_keyring_class, legacy_names))
|
||||
[...]
|
||||
"""
|
||||
module_name, sep, class_name = keyring_name.rpartition('.')
|
||||
__import__(module_name)
|
||||
module = sys.modules[module_name]
|
||||
return getattr(module, class_name)
|
||||
|
||||
|
||||
def load_keyring(keyring_name):
|
||||
"""
|
||||
Load the specified keyring by name (a fully-qualified name to the
|
||||
keyring, such as 'keyring.backends.file.PlaintextKeyring')
|
||||
"""
|
||||
class_ = _load_keyring_class(keyring_name)
|
||||
# invoke the priority to ensure it is viable, or raise a RuntimeError
|
||||
class_.priority
|
||||
return class_()
|
||||
|
||||
|
||||
def load_env():
|
||||
"""Load a keyring configured in the environment variable."""
|
||||
try:
|
||||
return load_keyring(os.environ['PYTHON_KEYRING_BACKEND'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Load a keyring using the config file in the config root."""
|
||||
|
||||
filename = 'keyringrc.cfg'
|
||||
|
||||
keyring_cfg = os.path.join(platform.config_root(), filename)
|
||||
|
||||
if not os.path.exists(keyring_cfg):
|
||||
return
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(keyring_cfg)
|
||||
_load_keyring_path(config)
|
||||
|
||||
# load the keyring class name, and then load this keyring
|
||||
try:
|
||||
if config.has_section("backend"):
|
||||
keyring_name = config.get("backend", "default-keyring").strip()
|
||||
else:
|
||||
raise configparser.NoOptionError('backend', 'default-keyring')
|
||||
|
||||
except (configparser.NoOptionError, ImportError):
|
||||
logger = logging.getLogger('keyring')
|
||||
logger.warning("Keyring config file contains incorrect values.\n"
|
||||
+ "Config file: %s" % keyring_cfg)
|
||||
return
|
||||
|
||||
return load_keyring(keyring_name)
|
||||
|
||||
|
||||
def _load_keyring_path(config):
|
||||
"load the keyring-path option (if present)"
|
||||
try:
|
||||
path = config.get("backend", "keyring-path").strip()
|
||||
sys.path.insert(0, path)
|
||||
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||
pass
|
||||
|
||||
|
||||
# init the _keyring_backend
|
||||
init_backend()
|
|
@ -0,0 +1,63 @@
|
|||
import os
|
||||
import abc
|
||||
|
||||
from .py27compat import add_metaclass
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
@add_metaclass(abc.ABCMeta)
|
||||
class Credential:
|
||||
"""Abstract class to manage credentials
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def username(self):
|
||||
return None
|
||||
|
||||
@abc.abstractproperty
|
||||
def password(self):
|
||||
return None
|
||||
|
||||
|
||||
class SimpleCredential(Credential):
|
||||
"""Simple credentials implementation
|
||||
"""
|
||||
|
||||
def __init__(self, username, password):
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self._username
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
|
||||
class EnvironCredential(Credential):
|
||||
"""Source credentials from environment variables.
|
||||
Actual sourcing is deferred until requested.
|
||||
"""
|
||||
|
||||
def __init__(self, user_env_var, pwd_env_var):
|
||||
self.user_env_var = user_env_var
|
||||
self.pwd_env_var = pwd_env_var
|
||||
|
||||
def _get_env(self, env_var):
|
||||
"""Helper to read an environment variable
|
||||
"""
|
||||
value = os.environ.get(env_var)
|
||||
if not value:
|
||||
raise ValueError('Missing environment variable:%s' % env_var)
|
||||
return value
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self._get_env(self.user_env_var)
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self._get_env(self.pwd_env_var)
|
|
@ -0,0 +1,11 @@
|
|||
from pluggy import HookimplMarker
|
||||
|
||||
import keyring
|
||||
|
||||
|
||||
hookimpl = HookimplMarker("devpiclient")
|
||||
|
||||
|
||||
@hookimpl()
|
||||
def devpiclient_get_password(url, username):
|
||||
return keyring.get_password(url, username)
|
|
@ -0,0 +1,64 @@
|
|||
import sys
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class KeyringError(Exception):
|
||||
"""Base class for exceptions in keyring
|
||||
"""
|
||||
|
||||
|
||||
class PasswordSetError(KeyringError):
|
||||
"""Raised when the password can't be set.
|
||||
"""
|
||||
|
||||
|
||||
class PasswordDeleteError(KeyringError):
|
||||
"""Raised when the password can't be deleted.
|
||||
"""
|
||||
|
||||
|
||||
class InitError(KeyringError):
|
||||
"""Raised when the keyring could not be initialised
|
||||
"""
|
||||
|
||||
|
||||
class KeyringLocked(KeyringError):
|
||||
"""Raised when the keyring could not be initialised
|
||||
"""
|
||||
|
||||
|
||||
class ExceptionRaisedContext:
|
||||
"""
|
||||
An exception-trapping context that indicates whether an exception was
|
||||
raised.
|
||||
"""
|
||||
|
||||
def __init__(self, ExpectedException=Exception):
|
||||
self.ExpectedException = ExpectedException
|
||||
self.exc_info = None
|
||||
|
||||
def __enter__(self):
|
||||
self.exc_info = object.__new__(ExceptionInfo)
|
||||
return self.exc_info
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
self.exc_info.__init__(*exc_info)
|
||||
return self.exc_info.type and issubclass(
|
||||
self.exc_info.type, self.ExpectedException)
|
||||
|
||||
|
||||
class ExceptionInfo:
|
||||
def __init__(self, *info):
|
||||
if not info:
|
||||
info = sys.exc_info()
|
||||
self.type, self.value, _ = info
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Return True if an exception occurred
|
||||
"""
|
||||
return bool(self.type)
|
||||
|
||||
__nonzero__ = __bool__
|
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
urllib2.HTTPPasswordMgr object using the keyring, for use with the
|
||||
urllib2.HTTPBasicAuthHandler.
|
||||
|
||||
usage:
|
||||
import urllib2
|
||||
handlers = [urllib2.HTTPBasicAuthHandler(PasswordMgr())]
|
||||
urllib2.install_opener(handlers)
|
||||
urllib2.urlopen(...)
|
||||
|
||||
This will prompt for a password if one is required and isn't already
|
||||
in the keyring. Then, it adds it to the keyring for subsequent use.
|
||||
"""
|
||||
|
||||
import getpass
|
||||
|
||||
from . import get_password, delete_password, set_password
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class PasswordMgr:
|
||||
def get_username(self, realm, authuri):
|
||||
return getpass.getuser()
|
||||
|
||||
def add_password(self, realm, authuri, password):
|
||||
user = self.get_username(realm, authuri)
|
||||
set_password(realm, user, password)
|
||||
|
||||
def find_user_password(self, realm, authuri):
|
||||
user = self.get_username(realm, authuri)
|
||||
password = get_password(realm, user)
|
||||
if password is None:
|
||||
prompt = 'password for %(user)s@%(realm)s for '\
|
||||
'%(authuri)s: ' % vars()
|
||||
password = getpass.getpass(prompt)
|
||||
set_password(realm, user, password)
|
||||
return user, password
|
||||
|
||||
def clear_password(self, realm, authuri):
|
||||
user = self.get_username(realm, authuri)
|
||||
delete_password(realm, user)
|
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
Compatibility support for Python 2.7. Remove when Python 2.7 support is
|
||||
no longer required.
|
||||
"""
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser # noqa
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
input = input
|
||||
|
||||
try:
|
||||
text_type = unicode
|
||||
string_types = unicode, str
|
||||
except NameError:
|
||||
text_type = str
|
||||
string_types = str,
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle # noqa
|
||||
|
||||
try:
|
||||
from itertools import ifilter as filter
|
||||
except ImportError:
|
||||
filter = filter
|
||||
|
||||
# Taken from six.py
|
||||
|
||||
|
||||
def add_metaclass(metaclass):
|
||||
"""Class decorator for creating a class with a metaclass."""
|
||||
def wrapper(cls):
|
||||
orig_vars = cls.__dict__.copy()
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
for slots_var in orig_vars.get('__slots__', ()):
|
||||
orig_vars.pop(slots_var)
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
import __builtin__ as builtins # noqa
|
|
@ -0,0 +1,7 @@
|
|||
try:
|
||||
from collections import abc
|
||||
except ImportError:
|
||||
import collections as abc
|
||||
|
||||
|
||||
__all__ = ['abc']
|
|
@ -0,0 +1,31 @@
|
|||
"""
|
||||
Compatibility support for Python 3.3. Remove when Python 3.3 support is
|
||||
no longer required.
|
||||
"""
|
||||
|
||||
from .py27compat import builtins
|
||||
|
||||
|
||||
def max(*args, **kwargs):
|
||||
"""
|
||||
Add support for 'default' kwarg.
|
||||
|
||||
>>> max([], default='res')
|
||||
'res'
|
||||
|
||||
>>> max(default='res')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: ...
|
||||
|
||||
>>> max('a', 'b', default='other')
|
||||
'b'
|
||||
"""
|
||||
missing = object()
|
||||
default = kwargs.pop('default', missing)
|
||||
try:
|
||||
return builtins.max(*args, **kwargs)
|
||||
except ValueError as exc:
|
||||
if 'empty sequence' in str(exc) and default is not missing:
|
||||
return default
|
||||
raise
|
|
@ -0,0 +1,4 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
|
@ -0,0 +1,17 @@
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from ..test_backend import BackendBasicTests
|
||||
from keyring.backends import OS_X
|
||||
|
||||
|
||||
def is_osx_keychain_supported():
|
||||
return sys.platform in ('mac', 'darwin')
|
||||
|
||||
|
||||
@unittest.skipUnless(is_osx_keychain_supported(),
|
||||
"Need macOS")
|
||||
class OSXKeychainTestCase(BackendBasicTests, unittest.TestCase):
|
||||
|
||||
def init_keyring(self):
|
||||
return OS_X.Keyring()
|
|
@ -0,0 +1,31 @@
|
|||
import unittest
|
||||
|
||||
from ..test_backend import BackendBasicTests
|
||||
from keyring.backends import SecretService
|
||||
from .. import util
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
SecretService.Keyring.viable,
|
||||
"SecretStorage package is needed for SecretServiceKeyring")
|
||||
class SecretServiceKeyringTestCase(BackendBasicTests, unittest.TestCase):
|
||||
__test__ = True
|
||||
|
||||
def init_keyring(self):
|
||||
print("Testing SecretServiceKeyring; the following "
|
||||
"password prompts are for this keyring")
|
||||
keyring = SecretService.Keyring()
|
||||
keyring.preferred_collection = (
|
||||
'/org/freedesktop/secrets/collection/session'
|
||||
)
|
||||
return keyring
|
||||
|
||||
|
||||
class SecretServiceKeyringUnitTests(unittest.TestCase):
|
||||
def test_supported_no_secretstorage(self):
|
||||
"""
|
||||
SecretService Keyring is not viable if secretstorage can't be imported.
|
||||
"""
|
||||
with util.NoNoneDictMutator(
|
||||
SecretService.__dict__, secretstorage=None):
|
||||
self.assertFalse(SecretService.Keyring.viable)
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
import keyring.backends.Windows
|
||||
from ..test_backend import BackendBasicTests
|
||||
|
||||
|
||||
@unittest.skipUnless(keyring.backends.Windows.WinVaultKeyring.viable,
|
||||
"Needs Windows")
|
||||
class WinVaultKeyringTestCase(BackendBasicTests, unittest.TestCase):
|
||||
def tearDown(self):
|
||||
# clean up any credentials created
|
||||
for cred in self.credentials_created:
|
||||
try:
|
||||
self.keyring.delete_password(*cred)
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
def init_keyring(self):
|
||||
return keyring.backends.Windows.WinVaultKeyring()
|
||||
|
||||
|
||||
@pytest.mark.skipif('sys.platform != "win32"')
|
||||
def test_winvault_always_viable():
|
||||
"""
|
||||
The WinVault backend should always be viable on Windows.
|
||||
"""
|
||||
assert keyring.backends.Windows.WinVaultKeyring.viable
|
|
@ -0,0 +1,37 @@
|
|||
import pytest
|
||||
|
||||
import keyring.backends.chainer
|
||||
from keyring import backend
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_keyrings(monkeypatch):
|
||||
def get_two():
|
||||
class Keyring1(backend.KeyringBackend):
|
||||
priority = 1
|
||||
|
||||
def get_password(self, system, user):
|
||||
return 'ring1-{system}-{user}'.format(**locals())
|
||||
|
||||
def set_password(self, system, user, password):
|
||||
pass
|
||||
|
||||
class Keyring2(backend.KeyringBackend):
|
||||
priority = 2
|
||||
|
||||
def get_password(self, system, user):
|
||||
return 'ring2-{system}-{user}'.format(**locals())
|
||||
|
||||
def set_password(self, system, user, password):
|
||||
raise NotImplementedError()
|
||||
|
||||
return Keyring1(), Keyring2()
|
||||
|
||||
monkeypatch.setattr('keyring.backend.get_all_keyring', get_two)
|
||||
|
||||
|
||||
class TestChainer:
|
||||
def test_chainer_gets_from_highest_priority(self, two_keyrings):
|
||||
chainer = keyring.backends.chainer.ChainerBackend()
|
||||
pw = chainer.get_password('alpha', 'bravo')
|
||||
assert pw == 'ring2-alpha-bravo'
|
|
@ -0,0 +1,84 @@
|
|||
import unittest
|
||||
|
||||
from keyring.backends import kwallet
|
||||
from ..test_backend import BackendBasicTests
|
||||
|
||||
|
||||
@unittest.skipUnless(kwallet.DBusKeyring.viable, "KWallet5 unavailable")
|
||||
class DBusKWalletTestCase(BackendBasicTests, unittest.TestCase):
|
||||
|
||||
# Remove '@' from service name as this is not supported in service names
|
||||
# '@' will cause troubles during migration of kwallet entries
|
||||
DIFFICULT_CHARS = BackendBasicTests.DIFFICULT_CHARS.replace('@', '')
|
||||
|
||||
def init_keyring(self):
|
||||
return kwallet.DBusKeyring()
|
||||
|
||||
def tearDown(self):
|
||||
for item in self.credentials_created:
|
||||
# Suppress errors, as only one pre/post migration item will be
|
||||
# present
|
||||
try:
|
||||
self.keyring.delete_password(*item)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
# TODO Remove empty folders created during tests
|
||||
|
||||
def set_password(self, service, username, password, old_format=False):
|
||||
# set the password and save the result so the test runner can clean
|
||||
# up after if necessary.
|
||||
self.credentials_created.add((service, username))
|
||||
|
||||
if old_format:
|
||||
username = username + '@' + service
|
||||
service = 'Python'
|
||||
|
||||
super(
|
||||
DBusKWalletTestCase,
|
||||
self).set_password(
|
||||
service,
|
||||
username,
|
||||
password)
|
||||
|
||||
def check_set_get(self, service, username, password):
|
||||
keyring = self.keyring
|
||||
|
||||
# for the non-existent password
|
||||
self.assertEqual(keyring.get_password(service, username), None)
|
||||
|
||||
# common usage
|
||||
self.set_password(service, username, password, True)
|
||||
# re-init keyring to force migration
|
||||
self.keyring = keyring = self.init_keyring()
|
||||
ret_password = keyring.get_password(service, username)
|
||||
self.assertEqual(
|
||||
ret_password, password,
|
||||
"Incorrect password for username: '%s' "
|
||||
"on service: '%s'. '%s' != '%s'"
|
||||
% (service, username, ret_password, password))
|
||||
|
||||
# for the empty password
|
||||
self.set_password(service, username, "", True)
|
||||
# re-init keyring to force migration
|
||||
self.keyring = keyring = self.init_keyring()
|
||||
ret_password = keyring.get_password(service, username)
|
||||
self.assertEqual(
|
||||
ret_password, "",
|
||||
"Incorrect password for username: '%s' "
|
||||
"on service: '%s'. '%s' != '%s'"
|
||||
% (service, username, ret_password, ""))
|
||||
ret_password = keyring.get_password('Python', username + '@' + service)
|
||||
self.assertEqual(
|
||||
ret_password, None,
|
||||
"Not 'None' password returned for username: '%s' "
|
||||
"on service: '%s'. '%s' != '%s'. Passwords from old "
|
||||
"folder should be deleted during migration."
|
||||
% (service, username, ret_password, None))
|
||||
|
||||
|
||||
@unittest.skipUnless(kwallet.DBusKeyringKWallet4.viable,
|
||||
"KWallet4 unavailable")
|
||||
class DBusKWallet4TestCase(DBusKWalletTestCase):
|
||||
def init_keyring(self):
|
||||
return kwallet.DBusKeyringKWallet4()
|
|
@ -0,0 +1,158 @@
|
|||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Common test functionality for backends.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import string
|
||||
|
||||
import pytest
|
||||
|
||||
from .util import random_string
|
||||
from keyring import errors
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
# unicode only characters
|
||||
# Sourced from The Quick Brown Fox... Pangrams
|
||||
# http://www.columbia.edu/~fdc/utf8/
|
||||
UNICODE_CHARS = (
|
||||
"זהכיףסתםלשמועאיךתנצחקרפדעץטובבגן"
|
||||
"ξεσκεπάζωτηνψυχοφθόραβδελυγμία"
|
||||
"Съешьжеещёэтихмягкихфранцузскихбулокдавыпейчаю"
|
||||
"Жълтатадюлябешещастливачепухъткойтоцъфназамръзнакатогьон"
|
||||
)
|
||||
|
||||
# ensure no-ascii chars slip by - watch your editor!
|
||||
assert min(ord(char) for char in UNICODE_CHARS) > 127
|
||||
|
||||
|
||||
def is_ascii_printable(s):
|
||||
return all(32 <= ord(c) < 127 for c in s)
|
||||
|
||||
|
||||
class BackendBasicTests:
|
||||
"""Test for the keyring's basic functions. password_set and password_get
|
||||
"""
|
||||
|
||||
DIFFICULT_CHARS = string.whitespace + string.punctuation
|
||||
|
||||
def setUp(self):
|
||||
self.keyring = self.init_keyring()
|
||||
self.credentials_created = set()
|
||||
|
||||
def tearDown(self):
|
||||
for item in self.credentials_created:
|
||||
self.keyring.delete_password(*item)
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
# set the password and save the result so the test runner can clean
|
||||
# up after if necessary.
|
||||
self.keyring.set_password(service, username, password)
|
||||
self.credentials_created.add((service, username))
|
||||
|
||||
def check_set_get(self, service, username, password):
|
||||
keyring = self.keyring
|
||||
|
||||
# for the non-existent password
|
||||
assert keyring.get_password(service, username) is None
|
||||
|
||||
# common usage
|
||||
self.set_password(service, username, password)
|
||||
assert keyring.get_password(service, username) == password
|
||||
|
||||
# for the empty password
|
||||
self.set_password(service, username, "")
|
||||
assert keyring.get_password(service, username) == ""
|
||||
|
||||
def test_password_set_get(self):
|
||||
password = random_string(20)
|
||||
username = random_string(20)
|
||||
service = random_string(20)
|
||||
self.check_set_get(service, username, password)
|
||||
|
||||
def test_difficult_chars(self):
|
||||
password = random_string(20, self.DIFFICULT_CHARS)
|
||||
username = random_string(20, self.DIFFICULT_CHARS)
|
||||
service = random_string(20, self.DIFFICULT_CHARS)
|
||||
self.check_set_get(service, username, password)
|
||||
|
||||
def test_delete_present(self):
|
||||
password = random_string(20, self.DIFFICULT_CHARS)
|
||||
username = random_string(20, self.DIFFICULT_CHARS)
|
||||
service = random_string(20, self.DIFFICULT_CHARS)
|
||||
self.keyring.set_password(service, username, password)
|
||||
self.keyring.delete_password(service, username)
|
||||
assert self.keyring.get_password(service, username) is None
|
||||
|
||||
def test_delete_not_present(self):
|
||||
username = random_string(20, self.DIFFICULT_CHARS)
|
||||
service = random_string(20, self.DIFFICULT_CHARS)
|
||||
with pytest.raises(errors.PasswordDeleteError):
|
||||
self.keyring.delete_password(service, username)
|
||||
|
||||
def test_delete_one_in_group(self):
|
||||
username1 = random_string(20, self.DIFFICULT_CHARS)
|
||||
username2 = random_string(20, self.DIFFICULT_CHARS)
|
||||
password = random_string(20, self.DIFFICULT_CHARS)
|
||||
service = random_string(20, self.DIFFICULT_CHARS)
|
||||
self.keyring.set_password(service, username1, password)
|
||||
self.set_password(service, username2, password)
|
||||
self.keyring.delete_password(service, username1)
|
||||
assert self.keyring.get_password(service, username2) == password
|
||||
|
||||
def test_name_property(self):
|
||||
assert is_ascii_printable(self.keyring.name)
|
||||
|
||||
def test_unicode_chars(self):
|
||||
password = random_string(20, UNICODE_CHARS)
|
||||
username = random_string(20, UNICODE_CHARS)
|
||||
service = random_string(20, UNICODE_CHARS)
|
||||
self.check_set_get(service, username, password)
|
||||
|
||||
def test_unicode_and_ascii_chars(self):
|
||||
source = (random_string(10, UNICODE_CHARS) + random_string(10)
|
||||
+ random_string(10, self.DIFFICULT_CHARS))
|
||||
password = random_string(20, source)
|
||||
username = random_string(20, source)
|
||||
service = random_string(20, source)
|
||||
self.check_set_get(service, username, password)
|
||||
|
||||
def test_different_user(self):
|
||||
"""
|
||||
Issue #47 reports that WinVault isn't storing passwords for
|
||||
multiple users. This test exercises that test for each of the
|
||||
backends.
|
||||
"""
|
||||
|
||||
keyring = self.keyring
|
||||
self.set_password('service1', 'user1', 'password1')
|
||||
self.set_password('service1', 'user2', 'password2')
|
||||
assert keyring.get_password('service1', 'user1') == 'password1'
|
||||
assert keyring.get_password('service1', 'user2') == 'password2'
|
||||
self.set_password('service2', 'user3', 'password3')
|
||||
assert keyring.get_password('service1', 'user1') == 'password1'
|
||||
|
||||
def test_credential(self):
|
||||
keyring = self.keyring
|
||||
|
||||
cred = keyring.get_credential('service', None)
|
||||
assert cred is None
|
||||
|
||||
self.set_password('service1', 'user1', 'password1')
|
||||
self.set_password('service1', 'user2', 'password2')
|
||||
|
||||
cred = keyring.get_credential('service1', None)
|
||||
assert cred is None or (cred.username, cred.password) in (
|
||||
('user1', 'password1'),
|
||||
('user2', 'password2'),
|
||||
)
|
||||
|
||||
cred = keyring.get_credential('service1', 'user2')
|
||||
assert cred is not None
|
||||
assert (cred.username, cred.password) in (
|
||||
('user1', 'password1'),
|
||||
('user2', 'password2'),
|
||||
)
|
|
@ -0,0 +1,74 @@
|
|||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ImportKiller:
|
||||
"Context manager to make an import of a given name or names fail."
|
||||
|
||||
def __init__(self, *names):
|
||||
self.names = names
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname in self.names:
|
||||
return self
|
||||
|
||||
def load_module(self, fullname):
|
||||
assert fullname in self.names
|
||||
raise ImportError(fullname)
|
||||
|
||||
def __enter__(self):
|
||||
self.original = {}
|
||||
for name in self.names:
|
||||
self.original[name] = sys.modules.pop(name, None)
|
||||
sys.meta_path.insert(0, self)
|
||||
|
||||
def __exit__(self, *args):
|
||||
sys.meta_path.remove(self)
|
||||
for key, value in self.original.items():
|
||||
if value is not None:
|
||||
sys.modules[key] = value
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def NoNoneDictMutator(destination, **changes):
|
||||
"""Helper context manager to make and unmake changes to a dict.
|
||||
|
||||
A None is not a valid value for the destination, and so means that the
|
||||
associated name should be removed."""
|
||||
original = {}
|
||||
for key, value in changes.items():
|
||||
original[key] = destination.get(key)
|
||||
if value is None:
|
||||
if key in destination:
|
||||
del destination[key]
|
||||
else:
|
||||
destination[key] = value
|
||||
yield
|
||||
for key, value in original.items():
|
||||
if value is None:
|
||||
if key in destination:
|
||||
del destination[key]
|
||||
else:
|
||||
destination[key] = value
|
||||
|
||||
|
||||
def Environ(**changes):
|
||||
"""A context manager to temporarily change the os.environ"""
|
||||
return NoNoneDictMutator(os.environ, **changes)
|
||||
|
||||
|
||||
ALPHABET = string.ascii_letters + string.digits
|
||||
|
||||
|
||||
def random_string(k, source=ALPHABET):
|
||||
"""Generate a random string with length <i>k</i>
|
||||
"""
|
||||
result = ''
|
||||
for i in range(0, k):
|
||||
result += random.choice(source)
|
||||
return result
|
|
@ -0,0 +1,35 @@
|
|||
import functools
|
||||
|
||||
|
||||
def once(func):
|
||||
"""
|
||||
Decorate func so it's only ever called the first time.
|
||||
|
||||
This decorator can ensure that an expensive or non-idempotent function
|
||||
will not be expensive on subsequent calls and is idempotent.
|
||||
|
||||
>>> func = once(lambda a: a+3)
|
||||
>>> func(3)
|
||||
6
|
||||
>>> func(9)
|
||||
6
|
||||
>>> func('12')
|
||||
6
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
if not hasattr(func, 'always_returns'):
|
||||
func.always_returns = func(*args, **kwargs)
|
||||
return func.always_returns
|
||||
return functools.wraps(func)(wrapper)
|
||||
|
||||
|
||||
def suppress_exceptions(callables, exceptions=Exception):
|
||||
"""
|
||||
yield the results of calling each element of callables, suppressing
|
||||
any indicated exceptions.
|
||||
"""
|
||||
for callable in callables:
|
||||
try:
|
||||
yield callable()
|
||||
except exceptions:
|
||||
pass
|
|
@ -0,0 +1,69 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
||||
|
||||
def _settings_root_XP():
|
||||
return os.path.join(os.environ['USERPROFILE'], 'Local Settings')
|
||||
|
||||
|
||||
def _settings_root_Vista():
|
||||
return os.environ.get('LOCALAPPDATA', os.environ.get('ProgramData', '.'))
|
||||
|
||||
|
||||
def _data_root_Windows():
|
||||
release, version, csd, ptype = platform.win32_ver()
|
||||
root = _settings_root_XP() if release == 'XP' else _settings_root_Vista()
|
||||
return os.path.join(root, 'Python Keyring')
|
||||
|
||||
|
||||
def _data_root_Linux():
|
||||
"""
|
||||
Use freedesktop.org Base Dir Specfication to determine storage
|
||||
location.
|
||||
"""
|
||||
fallback = os.path.expanduser('~/.local/share')
|
||||
root = os.environ.get('XDG_DATA_HOME', None) or fallback
|
||||
return os.path.join(root, 'python_keyring')
|
||||
|
||||
|
||||
_config_root_Windows = _data_root_Windows
|
||||
|
||||
|
||||
def _check_old_config_root():
|
||||
"""
|
||||
Prior versions of keyring would search for the config
|
||||
in XDG_DATA_HOME, but should probably have been
|
||||
searching for config in XDG_CONFIG_HOME. If the
|
||||
config exists in the former but not in the latter,
|
||||
raise a RuntimeError to force the change.
|
||||
"""
|
||||
# disable the check - once is enough and avoids infinite loop
|
||||
globals()['_check_old_config_root'] = lambda: None
|
||||
config_file_new = os.path.join(_config_root_Linux(), 'keyringrc.cfg')
|
||||
config_file_old = os.path.join(_data_root_Linux(), 'keyringrc.cfg')
|
||||
if os.path.isfile(config_file_old) and not os.path.isfile(config_file_new):
|
||||
msg = ("Keyring config exists only in the old location "
|
||||
"{config_file_old} and should be moved to {config_file_new} "
|
||||
"to work with this version of keyring.")
|
||||
raise RuntimeError(msg.format(**locals()))
|
||||
|
||||
|
||||
def _config_root_Linux():
|
||||
"""
|
||||
Use freedesktop.org Base Dir Specfication to determine config
|
||||
location.
|
||||
"""
|
||||
_check_old_config_root()
|
||||
fallback = os.path.expanduser('~/.local/share')
|
||||
key = 'XDG_CONFIG_HOME'
|
||||
root = os.environ.get(key, None) or fallback
|
||||
return os.path.join(root, 'python_keyring')
|
||||
|
||||
|
||||
# by default, use Unix convention
|
||||
data_root = globals().get('_data_root_' + platform.system(), _data_root_Linux)
|
||||
config_root = globals().get(
|
||||
'_config_root' + platform.system(),
|
||||
_config_root_Linux)
|
|
@ -0,0 +1,58 @@
|
|||
from ..py32compat import abc
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ClassProperty(property):
|
||||
"""
|
||||
An implementation of a property callable on a class. Used to decorate a
|
||||
classmethod but to then treat it like a property.
|
||||
|
||||
Example:
|
||||
|
||||
>>> class MyClass:
|
||||
... @ClassProperty
|
||||
... @classmethod
|
||||
... def skillz(cls):
|
||||
... return cls.__name__.startswith('My')
|
||||
>>> MyClass.skillz
|
||||
True
|
||||
>>> class YourClass(MyClass): pass
|
||||
>>> YourClass.skillz
|
||||
False
|
||||
"""
|
||||
|
||||
def __get__(self, cls, owner):
|
||||
return self.fget.__get__(None, owner)()
|
||||
|
||||
# borrowed from jaraco.util.dictlib
|
||||
|
||||
|
||||
class NonDataProperty:
|
||||
"""Much like the property builtin, but only implements __get__,
|
||||
making it a non-data property, and can be subsequently reset.
|
||||
|
||||
See http://users.rcn.com/python/download/Descriptor.htm for more
|
||||
information.
|
||||
|
||||
>>> class X:
|
||||
... @NonDataProperty
|
||||
... def foo(self):
|
||||
... return 3
|
||||
>>> x = X()
|
||||
>>> x.foo
|
||||
3
|
||||
>>> x.foo = 4
|
||||
>>> x.foo
|
||||
4
|
||||
"""
|
||||
|
||||
def __init__(self, fget):
|
||||
assert fget is not None, "fget cannot be none"
|
||||
assert isinstance(fget, abc.Callable), "fget must be callable"
|
||||
self.fget = fget
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
return self
|
||||
return self.fget(obj)
|
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"]
|
||||
build-backend = "setuptools.build_meta"
|
|
@ -0,0 +1,11 @@
|
|||
[pytest]
|
||||
norecursedirs=dist build .tox .eggs
|
||||
addopts=--doctest-modules --flake8
|
||||
doctest_optionflags=ALLOW_UNICODE ELLIPSIS
|
||||
filterwarnings=
|
||||
ignore:Possible nested set::pycodestyle:113
|
||||
ignore:Using or importing the ABCs::flake8:410
|
||||
# workaround for https://sourceforge.net/p/docutils/bugs/348/
|
||||
ignore:'U' mode is deprecated::docutils.io
|
||||
# workaround for https://gitlab.com/pycqa/flake8/issues/275
|
||||
ignore:You passed a bytestring as `filenames`.::flake8
|
|
@ -0,0 +1,63 @@
|
|||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
name = keyring
|
||||
author = Kang Zhang
|
||||
author_email = jobo.zh@gmail.com
|
||||
maintainer = Jason R. Coombs
|
||||
maintainer_email = jaraco@jaraco.com
|
||||
description = Store and access your passwords safely.
|
||||
long_description = file:README.rst
|
||||
url = https://github.com/jaraco/keyring
|
||||
classifiers =
|
||||
Development Status :: 5 - Production/Stable
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
License :: OSI Approved :: Python Software Foundation License
|
||||
License :: OSI Approved :: MIT License
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3.3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
include_package_data = true
|
||||
python_requires = >=2.7
|
||||
install_requires =
|
||||
entrypoints
|
||||
pywin32-ctypes!=0.1.0,!=0.1.1; sys_platform=="win32"
|
||||
secretstorage; sys_platform=="linux" and python_version>="3.5"
|
||||
secretstorage<3; (sys_platform=="linux2" or sys_platform=="linux") and python_version<"3.5"
|
||||
setup_requires = setuptools_scm >= 1.15.0
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
pytest >= 3.5, !=3.7.3
|
||||
pytest-checkdocs
|
||||
pytest-flake8
|
||||
docs =
|
||||
sphinx
|
||||
jaraco.packaging >= 3.2
|
||||
rst.linker >= 1.9
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
keyring=keyring.cli:main
|
||||
devpi_client =
|
||||
keyring = keyring.devpi_client
|
||||
keyring.backends =
|
||||
Windows = keyring.backends.Windows
|
||||
macOS = keyring.backends.OS_X
|
||||
SecretService = keyring.backends.SecretService
|
||||
KWallet = keyring.backends.kwallet
|
||||
chainer = keyring.backends.chainer
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import setuptools
|
||||
|
||||
if __name__ == "__main__":
|
||||
setuptools.setup(use_scm_version=True)
|
|
@ -0,0 +1,128 @@
|
|||
# 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.
|
||||
|
||||
## 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 `build-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:
|
||||
|
||||
- allow tabs for indentation (legacy for jaraco projects)
|
||||
- disable mutually incompatible rules W503 and W504.
|
||||
|
||||
## 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. In addition to running tests, an additional deploy stage is configured to automatically release tagged commits. The username and password for PyPI must be configured for each project using the `travis` command and only after the travis project is created. As releases are cut with [twine](https://pypi.org/project/twine), the two values are supplied through the `TWINE_USERNAME` and `TWINE_PASSWORD`. To configure the latter as a secret, run the following command:
|
||||
|
||||
```
|
||||
echo "TWINE_PASSWORD={password}" | travis encrypt
|
||||
```
|
||||
|
||||
Or disable it in the CI definition and configure it through the web UI.
|
||||
|
||||
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).
|
||||
|
||||
## 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 build-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.
|
|
@ -0,0 +1,15 @@
|
|||
import pkg_resources
|
||||
|
||||
|
||||
def test_entry_point():
|
||||
"""
|
||||
Keyring provides exactly one 'keyring' console script
|
||||
that's a callable.
|
||||
"""
|
||||
eps = pkg_resources.iter_entry_points('console_scripts')
|
||||
ep, = (
|
||||
ep
|
||||
for ep in eps
|
||||
if ep.name == 'keyring'
|
||||
)
|
||||
assert callable(ep.resolve())
|
|
@ -0,0 +1,31 @@
|
|||
[tox]
|
||||
envlist = python
|
||||
minversion = 2.4
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
setuptools>=31.0.1
|
||||
commands =
|
||||
pytest {posargs}
|
||||
usedevelop = True
|
||||
extras = testing
|
||||
|
||||
[testenv:build-docs]
|
||||
extras =
|
||||
docs
|
||||
testing
|
||||
changedir = docs
|
||||
commands =
|
||||
python -m sphinx . {toxinidir}/build/html
|
||||
|
||||
[testenv:release]
|
||||
skip_install = True
|
||||
deps =
|
||||
pep517>=0.5
|
||||
# workaround for https://github.com/pypa/twine/issues/423
|
||||
git+https://github.com/pypa/twine
|
||||
path.py
|
||||
commands =
|
||||
python -c "import path; path.Path('dist').rmtree_p()"
|
||||
python -m pep517.build .
|
||||
python -m twine upload dist/*
|
Loading…
Reference in New Issue