Remove keyring dependency. Fixes #190. By @Hofer-Julian

* Implement secretstorage backend for DBus connection
* Use newer Ubuntu version for Travis CI testing
This commit is contained in:
Manuel Riel 2019-03-06 09:37:12 +08:00 committed by GitHub
parent b115aef19b
commit a1d41612b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 110 additions and 70 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ dist/
docs/
*.autosave
__pycache__
.pytest_cache
.eggs
vorta.egg-info
.coverage

View File

@ -1,4 +1,6 @@
language: generic
sudo: required
dist: xenial
addons:
apt:
@ -10,6 +12,7 @@ addons:
- libacl1-dev
- libacl1
- build-essential
- libxkbcommon-x11-0
homebrew:
update: false
packages:
@ -27,9 +30,6 @@ cache:
- $HOME/.pyenv/versions
- $HOME/Library/Caches/Homebrew
sudo: required
dist: trusty
env:
global:
- SETUP_XVFB=true
@ -39,9 +39,9 @@ env:
matrix:
include:
- os: linux
dist: trusty
dist: xenial
env:
- RUN_PYINSTALLER=true
- RUN_PYINSTALLER=true
- os: osx
env:
- RUN_PYINSTALLER=true
@ -52,17 +52,13 @@ install:
export DISPLAY=:99.0
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset
sleep 3
# cd /opt/pyenv && git pull origin master
pyenv install -s $PYTHON36
eval "$(pyenv init -)"
pyenv shell $PYTHON36
elif [ $TRAVIS_OS_NAME = "osx" ]; then
brew upgrade pyenv
pyenv install -s $PYTHON37
pyenv install -s $PYTHON36
eval "$(pyenv init -)"
pyenv shell $PYTHON36 $PYTHON37
fi
pyenv install -s $PYTHON37
pyenv install -s $PYTHON36
eval "$(pyenv init -)"
pyenv shell $PYTHON36 $PYTHON37
- pip install -U setuptools pip==18.1
- pip install .
@ -70,7 +66,7 @@ install:
- pip install -r requirements.d/dev.txt
before_script:
- if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm )& fi
- if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm)& fi
- sleep 3
script:

View File

@ -36,10 +36,10 @@ install_requires =
pyqt5
peewee
python-dateutil
keyring
apscheduler
psutil
qdarkstyle
secretstorage; sys_platform != 'darwin'
pyobjc-core; sys_platform == 'darwin'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
pyobjc-framework-LaunchServices; sys_platform == 'darwin'
@ -83,7 +83,6 @@ deps =
pytest-mock
pytest-xdist
pytest-faulthandler
PyQt5==5.11.3
commands=pytest
passenv = DISPLAY

View File

@ -14,6 +14,7 @@ from subprocess import Popen, PIPE
from vorta.i18n import trans_late
from vorta.models import EventLogModel, BackupProfileMixin
from vorta.utils import keyring
from vorta.keyring.db import VortaDBKeyring
mutex = QtCore.QMutex()
logger = logging.getLogger(__name__)
@ -109,11 +110,18 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
return ret
# Try to get password from chosen keyring backend.
try:
ret['password'] = keyring.get_password("vorta-repo", profile.repo.url)
except Exception:
ret['message'] = trans_late('messages', 'Please make sure you grant Vorta permission to use the Keychain.')
return ret
logger.debug("Using %s keyring to store passwords.", keyring.__class__.__name__)
ret['password'] = keyring.get_password('vorta-repo', profile.repo.url)
# Try to fall back to DB Keyring, if we use the system keychain.
if ret['password'] is None and keyring.is_primary:
logger.debug('Password not found in primary keyring. Falling back to VortaDBKeyring.')
ret['password'] = VortaDBKeyring().get_password('vorta-repo', profile.repo.url)
# Give warning and continue if password is found there.
if ret['password'] is not None:
logger.warning('Found password in database, but secure storage was available. '
'Consider re-adding the repo to use it.')
ret['ssh_key'] = profile.ssh_key
ret['repo_id'] = profile.repo.id

39
src/vorta/keyring/abc.py Normal file
View File

@ -0,0 +1,39 @@
"""
Set the most appropriate Keyring backend for the current system.
For Linux not every system has SecretService available, so it will
fall back to a simple database keystore if needed.
"""
import sys
class VortaKeyring:
@classmethod
def get_keyring(cls):
if sys.platform == 'darwin': # Use Keychain on macOS
from .darwin import VortaDarwinKeyring
return VortaDarwinKeyring()
else: # Try to use DBus (available on Linux and *BSD)
import secretstorage
from .secretstorage import VortaSecretStorageKeyring
try:
return VortaSecretStorageKeyring()
except secretstorage.SecretServiceNotAvailableException: # Save passwords in DB, if all else fails.
from .db import VortaDBKeyring
return VortaDBKeyring()
def set_password(self, service, repo_url, password):
raise NotImplementedError
def get_password(self, service, repo_url):
"""
Retrieve a password from the underlying store. Return None if not found.
"""
raise NotImplementedError
@property
def is_primary(self):
"""
Return True if the current subclass is the system's primary keychain mechanism,
rather than a fallback (like our own VortaDBKeyring).
"""
return True

View File

@ -8,10 +8,10 @@ objc modules.
Adapted from https://gist.github.com/apettinen/5dc7bf1f6a07d148b2075725db6b1950
"""
from keyring.backend import KeyringBackend
from .abc import VortaKeyring
class VortaDarwinKeyring(KeyringBackend):
class VortaDarwinKeyring(VortaKeyring):
"""Homemade macOS Keychain Service"""
login_keychain = None
@ -42,10 +42,6 @@ class VortaDarwinKeyring(KeyringBackend):
result, login_keychain = SecKeychainOpen(b'login.keychain', None)
self.login_keychain = login_keychain
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
if not self.login_keychain: self._set_keychain()
@ -67,9 +63,6 @@ class VortaDarwinKeyring(KeyringBackend):
password = _resolve_password(password_length, password_buffer)
return password
def delete_password(self, service, repo_url):
pass
def _resolve_password(password_length, password_buffer):
from ctypes import c_char

View File

@ -1,18 +1,16 @@
import keyring
import peewee
from .abc import VortaKeyring
class VortaDBKeyring(keyring.backend.KeyringBackend):
class VortaDBKeyring(VortaKeyring):
"""
Our own fallback keyring service. Uses the main database
to store repo passwords if no other (more secure) backend
is available.
"""
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
from .models import RepoPassword
from vorta.models import RepoPassword
keyring_entry, created = RepoPassword.get_or_create(
url=repo_url,
defaults={'password': password}
@ -21,12 +19,13 @@ class VortaDBKeyring(keyring.backend.KeyringBackend):
keyring_entry.save()
def get_password(self, service, repo_url):
from .models import RepoPassword
from vorta.models import RepoPassword
try:
keyring_entry = RepoPassword.get(url=repo_url)
return keyring_entry.password
except Exception:
except peewee.DoesNotExist:
return None
def delete_password(self, service, repo_url):
pass
@property
def is_primary(self):
return False

View File

@ -0,0 +1,32 @@
import secretstorage
import asyncio
from vorta.keyring.abc import VortaKeyring
from vorta.log import logger
class VortaSecretStorageKeyring(VortaKeyring):
"""A wrapper for the secretstorage package to support the custom keyring backend"""
def __init__(self):
self.connection = secretstorage.dbus_init()
def set_password(self, service, repo_url, password):
asyncio.set_event_loop(asyncio.new_event_loop())
collection = secretstorage.get_default_collection(self.connection)
attributes = {
'application': 'Vorta',
'service': service,
'repo_url': repo_url,
'xdg:schema': 'org.freedesktop.Secret.Generic'}
collection.create_item(repo_url, attributes, password, replace=True)
def get_password(self, service, repo_url):
collection = secretstorage.get_default_collection(self.connection)
if collection.is_locked():
collection.unlock()
attributes = {'application': 'Vorta', 'service': service, 'repo_url': repo_url}
items = list(collection.search_items(attributes))
logger.debug('Found %i passwords matching repo URL.', len(items))
if len(items) > 0:
return items[0].get_secret().decode("utf-8")
return None

View File

@ -7,6 +7,7 @@ import unicodedata
import re
from datetime import datetime as dt
import getpass
import subprocess
from collections import defaultdict
from functools import reduce
import operator
@ -19,37 +20,9 @@ from paramiko import SSHException
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
import subprocess
import keyring
from vorta.keyring_db import VortaDBKeyring
from vorta.keyring.abc import VortaKeyring
"""
Set the most appropriate Keyring backend for the current system.
For macOS we use our own implementation due to conflicts between
Keyring and the autostart code.
For Linux not every system has SecretService available, so it will
fall back to a simple database keystore if needed.
"""
if sys.platform == 'darwin':
# from keyring.backends import OS_X
# keyring.set_keyring(OS_X.Keyring())
from vorta.keyring_darwin import VortaDarwinKeyring
keyring.set_keyring(VortaDarwinKeyring())
elif sys.platform == 'win32':
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
elif sys.platform == 'linux':
from keyring.backends import SecretService
try:
SecretService.Keyring.priority() # Test if keyring works.
keyring.set_keyring(SecretService.Keyring())
except Exception:
keyring.set_keyring(VortaDBKeyring())
else: # Fall back to saving password to database.
keyring.set_keyring(VortaDBKeyring())
keyring = VortaKeyring.get_keyring()
def nested_dict():