Skip to content

Commit

Permalink
Ensure system keychain is unlocked before using it. By @samuel-w (#607)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuel-w authored Dec 16, 2020
1 parent a4b49e7 commit 7bff91f
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 26 deletions.
5 changes: 5 additions & 0 deletions src/vorta/borg/borg_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down
12 changes: 9 additions & 3 deletions src/vorta/borg/info.py
Original file line number Diff line number Diff line change
@@ -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'])


Expand All @@ -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']
)
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/vorta/borg/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 19 additions & 1 deletion src/vorta/keyring/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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():
"""
Expand All @@ -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()
Expand Down
23 changes: 20 additions & 3 deletions src/vorta/keyring/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,31 @@ 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
result, login_keychain = SecKeychainOpen(b'login.keychain', None)
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,
Expand All @@ -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)
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/vorta/keyring/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 32 additions & 18 deletions src/vorta/keyring/secretstorage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import secretstorage
import asyncio
import sys

import secretstorage

from vorta.keyring.abc import VortaKeyring
from vorta.log import logger

Expand All @@ -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

0 comments on commit 7bff91f

Please sign in to comment.