1
0
Fork 0
mirror of https://github.com/borgbase/vorta synced 2025-03-12 07:09:37 +00:00

Ensure system keychain is unlocked before using it. By @samuel-w (#607)

This commit is contained in:
samuel-w 2020-12-15 20:56:42 -06:00 committed by GitHub
parent a4b49e7e0b
commit 7bff91fd21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 26 deletions

View file

@ -132,6 +132,11 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
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.')

View file

@ -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 @@ class BorgInfoThread(BorgThread):
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 @@ class BorgInfoThread(BorgThread):
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

View file

@ -14,7 +14,8 @@ class BorgInitThread(BorgThread):
# 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)

View file

@ -4,12 +4,16 @@ 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
_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 @@ class VortaKeyring:
"""
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()

View file

@ -23,18 +23,22 @@ class VortaDarwinKeyring(VortaKeyring):
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 @@ class VortaDarwinKeyring(VortaKeyring):
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 @@ class VortaDarwinKeyring(VortaKeyring):
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 @@ class VortaDarwinKeyring(VortaKeyring):
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

View file

@ -29,3 +29,7 @@ class VortaDBKeyring(VortaKeyring):
@property
def is_primary(self):
return False
@property
def is_unlocked(self):
return True

View file

@ -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 @@ class VortaSecretStorageKeyring(VortaKeyring):
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