Skip to content

Allows to fully disable using the system keychain. #898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 1, 2021
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- name: Test with pytest (macOS)
if: runner.os == 'macOS'
run: |
pytest
pytest --cov=vorta

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
Expand Down
6 changes: 4 additions & 2 deletions src/vorta/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ def eventFilter(self, source, event):
return False

def quit_app_action(self):
del self.main_window
self.scheduler.shutdown()
self.backup_cancelled_event.emit()
self.scheduler.shutdown()
del self.main_window
self.tray.deleteLater()
del self.tray
cleanup_db()

def create_backup_action(self, profile_id=None):
Expand Down
26 changes: 13 additions & 13 deletions src/vorta/borg/borg_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import time
import logging
from collections import namedtuple
from threading import Lock
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication
from subprocess import Popen, PIPE, TimeoutExpired
Expand All @@ -18,7 +19,7 @@
from vorta.keyring.abc import VortaKeyring
from vorta.keyring.db import VortaDBKeyring

mutex = QtCore.QMutex()
mutex = Lock()
logger = logging.getLogger(__name__)

FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments', 'encryption'])
Expand Down Expand Up @@ -93,11 +94,7 @@ def __init__(self, cmd, params, parent=None):

@classmethod
def is_running(cls):
if mutex.tryLock():
mutex.unlock()
return False
else:
return True
return mutex.locked()

@classmethod
def prepare(cls, profile):
Expand Down Expand Up @@ -139,11 +136,12 @@ def prepare(cls, profile):

# Check if keyring is locked
if profile.repo.encryption != 'none' and not cls.keyring.is_unlocked:
ret['message'] = trans_late('messages', 'Please unlock your password manager.')
ret['message'] = trans_late('messages',
'Please unlock your system password manager or disable it under Misc')
return ret

# Try to fall back to DB Keyring, if we use the system keychain.
if ret['password'] is None and cls.keyring.is_primary:
if ret['password'] is None and cls.keyring.is_system:
logger.debug('Password not found in primary keyring. Falling back to VortaDBKeyring.')
ret['password'] = VortaDBKeyring().get_password('vorta-repo', profile.repo.url)

Expand Down Expand Up @@ -189,7 +187,7 @@ def prepare_bin(cls):

def run(self):
self.started_event()
mutex.lock()
mutex.acquire()
log_entry = EventLogModel(category='borg-run',
subcommand=self.cmd[1],
profile=self.params.get('profile_name', None)
Expand Down Expand Up @@ -274,7 +272,7 @@ def read_async(fd):

self.process_result(result)
self.finished_event(result)
mutex.unlock()
mutex.release()

def cancel(self):
"""
Expand All @@ -286,9 +284,11 @@ def cancel(self):
try:
self.process.wait(timeout=3)
except TimeoutExpired:
self.process.terminate()
mutex.unlock()
self.terminate()
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.quit()
self.wait()
if mutex.locked():
mutex.release()

def process_result(self, result):
pass
Expand Down
75 changes: 37 additions & 38 deletions src/vorta/keyring/abc.py
Original file line number Diff line number Diff line change
@@ -1,49 +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
from pkg_resources import parse_version
import importlib
from vorta.i18n import trans_late


class VortaKeyring:
_keyring = None
all_keyrings = [
('.db', 'VortaDBKeyring'),
('.darwin', 'VortaDarwinKeyring'),
('.kwallet', 'VortaKWallet5Keyring'),
('.secretstorage', 'VortaSecretStorageKeyring')
]

@classmethod
def get_keyring(cls):
"""
Attempts to get secure keyring at runtime if current keyring is insecure.
Once it finds a secure keyring, it wil always use that keyring
Choose available Keyring. First assign a score and then try to initialize it.
"""
if cls._keyring is None or not cls._keyring.is_primary:
if sys.platform == 'darwin': # Use Keychain on macOS
from .darwin import VortaDarwinKeyring
cls._keyring = VortaDarwinKeyring()
else:
# Try to use KWallet (KDE)
from .kwallet import VortaKWallet5Keyring, KWalletNotAvailableException
try:
cls._keyring = VortaKWallet5Keyring()
except KWalletNotAvailableException:
# Try to use DBus and Gnome-Keyring (available on Linux and *BSD)
# Put this last as gnome keyring is included by default on many distros
import secretstorage
from .secretstorage import VortaSecretStorageKeyring
available_keyrings = []
for _module, _class in cls.all_keyrings:
try:
keyring = getattr(importlib.import_module(_module, package='vorta.keyring'), _class)
available_keyrings.append((keyring, keyring.get_priority()))
except Exception:
continue

# secretstorage has two different libraries based on version
if parse_version(secretstorage.__version__) >= parse_version("3.0.0"):
from jeepney.wrappers import DBusErrorResponse as DBusException
else:
from dbus.exceptions import DBusException
for keyring, _ in sorted(available_keyrings, key=lambda k: k[1], reverse=True):
try:
return keyring()
except Exception:
continue

try:
cls._keyring = VortaSecretStorageKeyring()
except (secretstorage.exceptions.SecretStorageException, DBusException):
# Save passwords in DB, if all else fails.
from .db import VortaDBKeyring
cls._keyring = VortaDBKeyring()
return cls._keyring
def get_backend_warning(self):
if self.is_system:
return trans_late('utils', 'Storing password in your password manager.')
else:
return trans_late('utils', 'Saving password with Vorta settings.')

def set_password(self, service, repo_url, password):
"""
Expand All @@ -58,12 +48,21 @@ def get_password(self, service, repo_url):
raise NotImplementedError

@property
def is_primary(self):
def is_system(self):
"""
Return True if the current subclass is the system's primary keychain mechanism,
rather than a fallback (like our own VortaDBKeyring).
"""
return True
raise NotImplementedError

@classmethod
def get_priority(cls):
"""
Return priority of this keyring on current system. Higher is more important.

Shout-out to https://github.com/jaraco/keyring for this idea.
"""
raise NotImplementedError

@property
def is_unlocked(self):
Expand Down
13 changes: 13 additions & 0 deletions src/vorta/keyring/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

Adapted from https://gist.github.com/apettinen/5dc7bf1f6a07d148b2075725db6b1950
"""
import sys
from .abc import VortaKeyring


Expand Down Expand Up @@ -79,8 +80,20 @@ def is_unlocked(self):

return keychain_status & kSecUnlockStateStatus

@classmethod
def get_priority(cls):
if sys.platform == 'darwin':
return 8
else:
raise RuntimeError('Only available on macOS')

@property
def is_system(self):
return True


def _resolve_password(password_length, password_buffer):
from ctypes import c_char
s = (c_char*password_length).from_address(password_buffer.__pointer__)[:]
return s.decode()

7 changes: 6 additions & 1 deletion src/vorta/keyring/db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import peewee
from .abc import VortaKeyring
from vorta.models import SettingsModel


class VortaDBKeyring(VortaKeyring):
Expand Down Expand Up @@ -27,9 +28,13 @@ def get_password(self, service, repo_url):
return None

@property
def is_primary(self):
def is_system(self):
return False

@property
def is_unlocked(self):
return True

@classmethod
def get_priority(cls):
return 1 if SettingsModel.get(key='use_system_keyring').value else 10
10 changes: 10 additions & 0 deletions src/vorta/keyring/kwallet.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from PyQt5 import QtDBus
from PyQt5.QtCore import QVariant
from vorta.keyring.abc import VortaKeyring
Expand All @@ -20,6 +21,7 @@ def __init__(self):
self.object_path,
self.interface_name,
QtDBus.QDBusConnection.sessionBus())
self.handle = -1
if not (self.iface.isValid() and self.get_result("isEnabled") is True):
raise KWalletNotAvailableException

Expand Down Expand Up @@ -54,6 +56,14 @@ def try_unlock(self):
except ValueError: # For when kwallet is disabled or dbus otherwise broken
self.handle = -2

@classmethod
def get_priority(cls):
return 6 if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "") else 4

@property
def is_system(self):
return True


class KWalletNotAvailableException(Exception):
pass
10 changes: 9 additions & 1 deletion src/vorta/keyring/secretstorage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import sys

import os
import secretstorage

from vorta.keyring.abc import VortaKeyring
Expand Down Expand Up @@ -52,3 +52,11 @@ def is_unlocked(self):
except secretstorage.exceptions.SecretServiceNotAvailableException:
logger.debug('SecretStorage is closed.')
return False

@classmethod
def get_priority(cls):
return 6 if "GNOME" in os.getenv("XDG_CURRENT_DESKTOP", "") else 5

@property
def is_system(self):
return True
6 changes: 6 additions & 0 deletions src/vorta/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ def get_misc_settings():
'label': trans_late('settings',
'Get statistics of file/folder when added')
},
{
'key': 'use_system_keyring', 'value': True, 'type': 'checkbox',
'label': trans_late('settings',
'Store repository passwords in system keychain, if available.')
},
{
'key': 'override_mount_permissions', 'value': False, 'type': 'checkbox',
'label': trans_late('settings',
Expand Down Expand Up @@ -274,6 +279,7 @@ def get_misc_settings():
def cleanup_db():
# Clean up database
db.execute_sql("VACUUM")
db.close()


def init_db(con=None):
Expand Down
12 changes: 0 additions & 12 deletions src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from vorta.borg._compatibility import BorgCompatibility
from vorta.i18n import trans_late
from vorta.keyring.abc import VortaKeyring
from vorta.log import logger
from vorta.network_status.abc import NetworkStatusMonitor

Expand Down Expand Up @@ -337,14 +336,3 @@ def validate_passwords(first_pass, second_pass):
return trans_late('utils', "Passwords must be greater than 8 characters long.")

return ""


def display_password_backend(encryption):
''' Display password backend message based off current keyring '''
# flake8: noqa E501
if encryption != 'none':
keyring = VortaKeyring.get_keyring()
return trans_late('utils', "Storing the password in your password manager.") if keyring.is_primary else trans_late(
'utils', 'Saving the password to disk. To store password more securely install a supported secret store such as KeepassXC')
else:
return ""
12 changes: 7 additions & 5 deletions src/vorta/views/repo_add_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from PyQt5.QtWidgets import QLineEdit, QAction

from vorta.utils import get_private_keys, get_asset, choose_file_dialog, \
borg_compat, validate_passwords, display_password_backend
borg_compat, validate_passwords
from vorta.keyring.abc import VortaKeyring
from vorta.borg.init import BorgInitThread
from vorta.borg.info_repo import BorgInfoRepoThread
Expand Down Expand Up @@ -31,7 +31,7 @@ def __init__(self, parent=None):
self.repoURL.textChanged.connect(self.set_password)
self.passwordLineEdit.textChanged.connect(self.password_listener)
self.confirmLineEdit.textChanged.connect(self.password_listener)
self.encryptionComboBox.activated.connect(self.display_password_backend)
self.encryptionComboBox.activated.connect(self.display_backend_warning)

# Add clickable icon to toggle password visibility to end of box
self.showHideAction = QAction(self.tr("Show my passwords"), self)
Expand All @@ -45,7 +45,7 @@ def __init__(self, parent=None):
self.init_encryption()
self.init_ssh_key()
self.set_icons()
self.display_password_backend()
self.display_backend_warning()

def set_icons(self):
self.chooseLocalFolderButton.setIcon(get_colored_icon('folder-open'))
Expand All @@ -64,8 +64,10 @@ def values(self):
out['encryption'] = self.encryptionComboBox.currentData()
return out

def display_password_backend(self):
self.passwordLabel.setText(translate('utils', display_password_backend(self.encryptionComboBox.currentData())))
def display_backend_warning(self):
'''Display password backend message based off current keyring'''
if self.encryptionComboBox.currentData() != 'none':
self.passwordLabel.setText(VortaKeyring.get_keyring().get_backend_warning())

def choose_local_backup_folder(self):
def receive():
Expand Down
Loading