From 7bff91fd2121ed0f858224adbb3b8fd6858c70d6 Mon Sep 17 00:00:00 2001 From: samuel-w Date: Tue, 15 Dec 2020 20:56:42 -0600 Subject: [PATCH] Ensure system keychain is unlocked before using it. By @samuel-w (#607) --- src/vorta/borg/borg_thread.py | 5 +++ src/vorta/borg/info.py | 12 +++++-- src/vorta/borg/init.py | 3 +- src/vorta/keyring/abc.py | 20 +++++++++++- src/vorta/keyring/darwin.py | 23 ++++++++++++-- src/vorta/keyring/db.py | 4 +++ src/vorta/keyring/secretstorage.py | 50 +++++++++++++++++++----------- 7 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src/vorta/borg/borg_thread.py b/src/vorta/borg/borg_thread.py index f79fd571a..53ccc39fa 100644 --- a/src/vorta/borg/borg_thread.py +++ b/src/vorta/borg/borg_thread.py @@ -132,6 +132,11 @@ def prepare(cls, profile): logger.debug("Using %s keyring to store passwords.", keyring.__class__.__name__) ret['password'] = keyring.get_password('vorta-repo', profile.repo.url) + # Check if keyring is locked + if profile.repo.encryption != 'none' and not keyring.is_unlocked: + ret['message'] = trans_late('messages', 'Please unlock your password manager.') + return ret + # 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.') diff --git a/src/vorta/borg/info.py b/src/vorta/borg/info.py index dbfd08dca..ce6def9e6 100644 --- a/src/vorta/borg/info.py +++ b/src/vorta/borg/info.py @@ -1,9 +1,10 @@ from collections import namedtuple from .borg_thread import BorgThread +from vorta.i18n import trans_late from vorta.models import RepoModel from vorta.keyring.abc import get_keyring -FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments']) +FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments', 'encryption']) FakeProfile = namedtuple('FakeProfile', ['repo', 'name', 'ssh_key']) @@ -18,9 +19,9 @@ def prepare(cls, params): Used to validate existing repository when added. """ - # Build fake profile because we don't have it in the DB yet. + # Build fake profile because we don't have it in the DB yet. Assume unencrypted. profile = FakeProfile( - FakeRepo(params['repo_url'], 999, params['extra_borg_arguments']), + FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'], 'none'), 'New Repo', params['ssh_key'] ) @@ -43,6 +44,11 @@ def prepare(cls, params): ret['password'] = '999999' # Dummy password if the user didn't supply one. To avoid prompt. else: ret['password'] = params['password'] + # Cannot tell if repo has encryption, assuming based off of password + if not get_keyring().is_unlocked: + ret['message'] = trans_late('messages', 'Please unlock your password manager.') + return ret + ret['ok'] = True ret['cmd'] = cmd diff --git a/src/vorta/borg/init.py b/src/vorta/borg/init.py index df7b936c2..6abe0a8f5 100644 --- a/src/vorta/borg/init.py +++ b/src/vorta/borg/init.py @@ -14,7 +14,8 @@ def prepare(cls, params): # Build fake profile because we don't have it in the DB yet. profile = FakeProfile( - FakeRepo(params['repo_url'], 999, params['extra_borg_arguments']), 'Init Repo', params['ssh_key'] + FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'], + params['encryption']), 'Init Repo', params['ssh_key'] ) ret = super().prepare(profile) diff --git a/src/vorta/keyring/abc.py b/src/vorta/keyring/abc.py index 5d48ce03a..6b0776da8 100644 --- a/src/vorta/keyring/abc.py +++ b/src/vorta/keyring/abc.py @@ -4,12 +4,16 @@ fall back to a simple database keystore if needed. """ import sys +from pkg_resources import parse_version _keyring = None class VortaKeyring: def set_password(self, service, repo_url, password): + """ + Writes a password to the underlying store. + """ raise NotImplementedError def get_password(self, service, repo_url): @@ -26,6 +30,13 @@ def is_primary(self): """ return True + @property + def is_unlocked(self): + """ + Returns True if the keyring is open. Return False if it is closed or locked + """ + raise NotImplementedError + def get_keyring(): """ @@ -40,9 +51,16 @@ def get_keyring(): else: # Try to use DBus and Gnome-Keyring (available on Linux and *BSD) import secretstorage from .secretstorage import VortaSecretStorageKeyring + + # 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 + try: _keyring = VortaSecretStorageKeyring() - except secretstorage.SecretServiceNotAvailableException: # Try to use KWallet + except (secretstorage.exceptions.SecretStorageException, DBusException): # Try to use KWallet (KDE) from .kwallet import VortaKWallet5Keyring, KWalletNotAvailableException try: _keyring = VortaKWallet5Keyring() diff --git a/src/vorta/keyring/darwin.py b/src/vorta/keyring/darwin.py index 703e47180..d5ccac7fd 100644 --- a/src/vorta/keyring/darwin.py +++ b/src/vorta/keyring/darwin.py @@ -23,18 +23,22 @@ def _set_keychain(self): from Foundation import NSBundle Security = NSBundle.bundleWithIdentifier_('com.apple.security') + # https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html S_functions = [ ('SecKeychainGetTypeID', b'I'), ('SecKeychainItemGetTypeID', b'I'), ('SecKeychainAddGenericPassword', b'i^{OpaqueSecKeychainRef=}I*I*I*o^^{OpaqueSecKeychainItemRef}'), ('SecKeychainOpen', b'i*o^^{OpaqueSecKeychainRef}'), ('SecKeychainFindGenericPassword', b'i@I*I*o^Io^^{OpaquePassBuff}o^^{OpaqueSecKeychainItemRef}'), + ('SecKeychainGetStatus', b'i^{OpaqueSecKeychainRef=}o^I'), ] objc.loadBundleFunctions(Security, globals(), S_functions) SecKeychainRef = objc.registerCFSignature('SecKeychainRef', b'^{OpaqueSecKeychainRef=}', SecKeychainGetTypeID()) - SecKeychainItemRef = objc.registerCFSignature('SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID()) + SecKeychainItemRef = objc.registerCFSignature( + 'SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID()) + PassBuffRef = objc.createOpaquePointerType('PassBuffRef', b'^{OpaquePassBuff=}', None) # Get the login keychain @@ -42,7 +46,8 @@ def _set_keychain(self): self.login_keychain = login_keychain def set_password(self, service, repo_url, password): - if not self.login_keychain: self._set_keychain() + if not self.login_keychain: + self._set_keychain() SecKeychainAddGenericPassword( self.login_keychain, @@ -52,7 +57,8 @@ def set_password(self, service, repo_url, password): None) def get_password(self, service, repo_url): - if not self.login_keychain: self._set_keychain() + if not self.login_keychain: + self._set_keychain() result, password_length, password_buffer, keychain_item = SecKeychainFindGenericPassword( self.login_keychain, len(service), service.encode(), len(repo_url), repo_url.encode(), None, None, None) @@ -62,6 +68,17 @@ def get_password(self, service, repo_url): password = _resolve_password(password_length, password_buffer) return password + @property + def is_unlocked(self): + kSecUnlockStateStatus = 1 + + if not self.login_keychain: + self._set_keychain() + + result, keychain_status = SecKeychainGetStatus(self.login_keychain, None) + + return keychain_status & kSecUnlockStateStatus + def _resolve_password(password_length, password_buffer): from ctypes import c_char diff --git a/src/vorta/keyring/db.py b/src/vorta/keyring/db.py index cca6ddb1f..fc8b0af7f 100644 --- a/src/vorta/keyring/db.py +++ b/src/vorta/keyring/db.py @@ -29,3 +29,7 @@ def get_password(self, service, repo_url): @property def is_primary(self): return False + + @property + def is_unlocked(self): + return True diff --git a/src/vorta/keyring/secretstorage.py b/src/vorta/keyring/secretstorage.py index 5162eacd2..b8c4ddf1e 100644 --- a/src/vorta/keyring/secretstorage.py +++ b/src/vorta/keyring/secretstorage.py @@ -1,5 +1,8 @@ -import secretstorage import asyncio +import sys + +import secretstorage + from vorta.keyring.abc import VortaKeyring from vorta.log import logger @@ -16,23 +19,34 @@ def __init__(self): secretstorage.get_default_collection(self.connection) 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) + try: + 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) + except secretstorage.exceptions.ItemNotFoundException: + logger.error("SecretStorage writing failed", exc_info=sys.exc_info()) def get_password(self, service, repo_url): - asyncio.set_event_loop(asyncio.new_event_loop()) - 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") + if self.is_unlocked: + asyncio.set_event_loop(asyncio.new_event_loop()) + collection = secretstorage.get_default_collection(self.connection) + 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 + + @property + def is_unlocked(self): + try: + collection = secretstorage.get_default_collection(self.connection) + return not collection.is_locked() + except secretstorage.exceptions.SecretServiceNotAvailableException: + logger.debug('SecretStorage is closed.') + return False