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:
parent
a4b49e7e0b
commit
7bff91fd21
7 changed files with 91 additions and 26 deletions
|
@ -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.')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -29,3 +29,7 @@ class VortaDBKeyring(VortaKeyring):
|
|||
@property
|
||||
def is_primary(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_unlocked(self):
|
||||
return True
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue