Skip to content

Commit 6d8ad90

Browse files
authored
Allow to fully disable using the system keychain. (#898)
- Add option to avoid using system keychain. - Prioritize keychains first. Then try to start them. - Maintenance: Possible fixes for hung tests and segfault on exit on some setups.
1 parent 824707c commit 6d8ad90

File tree

12 files changed

+116
-86
lines changed

12 files changed

+116
-86
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
- name: Test with pytest (macOS)
6060
if: runner.os == 'macOS'
6161
run: |
62-
pytest
62+
pytest --cov=vorta
6363
6464
- name: Upload coverage to Codecov
6565
uses: codecov/codecov-action@v1

src/vorta/application.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ def eventFilter(self, source, event):
102102
return False
103103

104104
def quit_app_action(self):
105-
del self.main_window
106-
self.scheduler.shutdown()
107105
self.backup_cancelled_event.emit()
106+
self.scheduler.shutdown()
107+
del self.main_window
108+
self.tray.deleteLater()
109+
del self.tray
108110
cleanup_db()
109111

110112
def create_backup_action(self, profile_id=None):

src/vorta/borg/borg_thread.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import time
99
import logging
1010
from collections import namedtuple
11+
from threading import Lock
1112
from PyQt5 import QtCore
1213
from PyQt5.QtWidgets import QApplication
1314
from subprocess import Popen, PIPE, TimeoutExpired
@@ -18,7 +19,7 @@
1819
from vorta.keyring.abc import VortaKeyring
1920
from vorta.keyring.db import VortaDBKeyring
2021

21-
mutex = QtCore.QMutex()
22+
mutex = Lock()
2223
logger = logging.getLogger(__name__)
2324

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

9495
@classmethod
9596
def is_running(cls):
96-
if mutex.tryLock():
97-
mutex.unlock()
98-
return False
99-
else:
100-
return True
97+
return mutex.locked()
10198

10299
@classmethod
103100
def prepare(cls, profile):
@@ -139,11 +136,12 @@ def prepare(cls, profile):
139136

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

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

@@ -189,7 +187,7 @@ def prepare_bin(cls):
189187

190188
def run(self):
191189
self.started_event()
192-
mutex.lock()
190+
mutex.acquire()
193191
log_entry = EventLogModel(category='borg-run',
194192
subcommand=self.cmd[1],
195193
profile=self.params.get('profile_name', None)
@@ -274,7 +272,7 @@ def read_async(fd):
274272

275273
self.process_result(result)
276274
self.finished_event(result)
277-
mutex.unlock()
275+
mutex.release()
278276

279277
def cancel(self):
280278
"""
@@ -286,9 +284,11 @@ def cancel(self):
286284
try:
287285
self.process.wait(timeout=3)
288286
except TimeoutExpired:
289-
self.process.terminate()
290-
mutex.unlock()
291-
self.terminate()
287+
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
288+
self.quit()
289+
self.wait()
290+
if mutex.locked():
291+
mutex.release()
292292

293293
def process_result(self, result):
294294
pass

src/vorta/keyring/abc.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,39 @@
1-
"""
2-
Set the most appropriate Keyring backend for the current system.
3-
For Linux not every system has SecretService available, so it will
4-
fall back to a simple database keystore if needed.
5-
"""
6-
import sys
7-
from pkg_resources import parse_version
1+
import importlib
2+
from vorta.i18n import trans_late
83

94

105
class VortaKeyring:
11-
_keyring = None
6+
all_keyrings = [
7+
('.db', 'VortaDBKeyring'),
8+
('.darwin', 'VortaDarwinKeyring'),
9+
('.kwallet', 'VortaKWallet5Keyring'),
10+
('.secretstorage', 'VortaSecretStorageKeyring')
11+
]
1212

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

34-
# secretstorage has two different libraries based on version
35-
if parse_version(secretstorage.__version__) >= parse_version("3.0.0"):
36-
from jeepney.wrappers import DBusErrorResponse as DBusException
37-
else:
38-
from dbus.exceptions import DBusException
26+
for keyring, _ in sorted(available_keyrings, key=lambda k: k[1], reverse=True):
27+
try:
28+
return keyring()
29+
except Exception:
30+
continue
3931

40-
try:
41-
cls._keyring = VortaSecretStorageKeyring()
42-
except (secretstorage.exceptions.SecretStorageException, DBusException):
43-
# Save passwords in DB, if all else fails.
44-
from .db import VortaDBKeyring
45-
cls._keyring = VortaDBKeyring()
46-
return cls._keyring
32+
def get_backend_warning(self):
33+
if self.is_system:
34+
return trans_late('utils', 'Storing password in your password manager.')
35+
else:
36+
return trans_late('utils', 'Saving password with Vorta settings.')
4737

4838
def set_password(self, service, repo_url, password):
4939
"""
@@ -58,12 +48,21 @@ def get_password(self, service, repo_url):
5848
raise NotImplementedError
5949

6050
@property
61-
def is_primary(self):
51+
def is_system(self):
6252
"""
6353
Return True if the current subclass is the system's primary keychain mechanism,
6454
rather than a fallback (like our own VortaDBKeyring).
6555
"""
66-
return True
56+
raise NotImplementedError
57+
58+
@classmethod
59+
def get_priority(cls):
60+
"""
61+
Return priority of this keyring on current system. Higher is more important.
62+
63+
Shout-out to https://github.com/jaraco/keyring for this idea.
64+
"""
65+
raise NotImplementedError
6766

6867
@property
6968
def is_unlocked(self):

src/vorta/keyring/darwin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
Adapted from https://gist.github.com/apettinen/5dc7bf1f6a07d148b2075725db6b1950
99
"""
10+
import sys
1011
from .abc import VortaKeyring
1112

1213

@@ -79,8 +80,20 @@ def is_unlocked(self):
7980

8081
return keychain_status & kSecUnlockStateStatus
8182

83+
@classmethod
84+
def get_priority(cls):
85+
if sys.platform == 'darwin':
86+
return 8
87+
else:
88+
raise RuntimeError('Only available on macOS')
89+
90+
@property
91+
def is_system(self):
92+
return True
93+
8294

8395
def _resolve_password(password_length, password_buffer):
8496
from ctypes import c_char
8597
s = (c_char*password_length).from_address(password_buffer.__pointer__)[:]
8698
return s.decode()
99+

src/vorta/keyring/db.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import peewee
22
from .abc import VortaKeyring
3+
from vorta.models import SettingsModel
34

45

56
class VortaDBKeyring(VortaKeyring):
@@ -27,9 +28,13 @@ def get_password(self, service, repo_url):
2728
return None
2829

2930
@property
30-
def is_primary(self):
31+
def is_system(self):
3132
return False
3233

3334
@property
3435
def is_unlocked(self):
3536
return True
37+
38+
@classmethod
39+
def get_priority(cls):
40+
return 1 if SettingsModel.get(key='use_system_keyring').value else 10

src/vorta/keyring/kwallet.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from PyQt5 import QtDBus
23
from PyQt5.QtCore import QVariant
34
from vorta.keyring.abc import VortaKeyring
@@ -20,6 +21,7 @@ def __init__(self):
2021
self.object_path,
2122
self.interface_name,
2223
QtDBus.QDBusConnection.sessionBus())
24+
self.handle = -1
2325
if not (self.iface.isValid() and self.get_result("isEnabled") is True):
2426
raise KWalletNotAvailableException
2527

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

59+
@classmethod
60+
def get_priority(cls):
61+
return 6 if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "") else 4
62+
63+
@property
64+
def is_system(self):
65+
return True
66+
5767

5868
class KWalletNotAvailableException(Exception):
5969
pass

src/vorta/keyring/secretstorage.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import sys
3-
3+
import os
44
import secretstorage
55

66
from vorta.keyring.abc import VortaKeyring
@@ -52,3 +52,11 @@ def is_unlocked(self):
5252
except secretstorage.exceptions.SecretServiceNotAvailableException:
5353
logger.debug('SecretStorage is closed.')
5454
return False
55+
56+
@classmethod
57+
def get_priority(cls):
58+
return 6 if "GNOME" in os.getenv("XDG_CURRENT_DESKTOP", "") else 5
59+
60+
@property
61+
def is_system(self):
62+
return True

src/vorta/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ def get_misc_settings():
225225
'label': trans_late('settings',
226226
'Get statistics of file/folder when added')
227227
},
228+
{
229+
'key': 'use_system_keyring', 'value': True, 'type': 'checkbox',
230+
'label': trans_late('settings',
231+
'Store repository passwords in system keychain, if available.')
232+
},
228233
{
229234
'key': 'override_mount_permissions', 'value': False, 'type': 'checkbox',
230235
'label': trans_late('settings',
@@ -274,6 +279,7 @@ def get_misc_settings():
274279
def cleanup_db():
275280
# Clean up database
276281
db.execute_sql("VACUUM")
282+
db.close()
277283

278284

279285
def init_db(con=None):

src/vorta/utils.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
from vorta.borg._compatibility import BorgCompatibility
2424
from vorta.i18n import trans_late
25-
from vorta.keyring.abc import VortaKeyring
2625
from vorta.log import logger
2726
from vorta.network_status.abc import NetworkStatusMonitor
2827

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

339338
return ""
340-
341-
342-
def display_password_backend(encryption):
343-
''' Display password backend message based off current keyring '''
344-
# flake8: noqa E501
345-
if encryption != 'none':
346-
keyring = VortaKeyring.get_keyring()
347-
return trans_late('utils', "Storing the password in your password manager.") if keyring.is_primary else trans_late(
348-
'utils', 'Saving the password to disk. To store password more securely install a supported secret store such as KeepassXC')
349-
else:
350-
return ""

src/vorta/views/repo_add_dialog.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from PyQt5.QtWidgets import QLineEdit, QAction
44

55
from vorta.utils import get_private_keys, get_asset, choose_file_dialog, \
6-
borg_compat, validate_passwords, display_password_backend
6+
borg_compat, validate_passwords
77
from vorta.keyring.abc import VortaKeyring
88
from vorta.borg.init import BorgInitThread
99
from vorta.borg.info_repo import BorgInfoRepoThread
@@ -31,7 +31,7 @@ def __init__(self, parent=None):
3131
self.repoURL.textChanged.connect(self.set_password)
3232
self.passwordLineEdit.textChanged.connect(self.password_listener)
3333
self.confirmLineEdit.textChanged.connect(self.password_listener)
34-
self.encryptionComboBox.activated.connect(self.display_password_backend)
34+
self.encryptionComboBox.activated.connect(self.display_backend_warning)
3535

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

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

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

7072
def choose_local_backup_folder(self):
7173
def receive():

0 commit comments

Comments
 (0)