Add own macOS Keychain implementation to avoid conflict with autostart. #81

This commit is contained in:
Manu 2018-12-06 17:41:22 +08:00
parent d8de3206dd
commit a2e9bf2886
6 changed files with 152 additions and 48 deletions

23
requirements.d/conda.yml Normal file
View File

@ -0,0 +1,23 @@
name: vorta
channels:
- conda-forge
- defaults
dependencies:
- python=3.6.7
- appdirs
- paramiko
- pyqt
- peewee
- python-dateutil
- keyring
- apscheduler
- sentry-sdk
- psutil
- pyobjc-core
- pyobjc-framework-Cocoa
- pyinstaller
- pip:
- sentry-sdk

View File

@ -41,6 +41,7 @@ install_requires =
psutil
pyobjc-core; sys_platform == 'darwin'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
pyobjc-framework-LaunchServices; sys_platform == 'darwin'
tests_require =
pytest
pytest-qt

View File

@ -0,0 +1,61 @@
"""
A dirty objc implementation to access the macOS Keychain. Because the
keyring implementation was causing trouble when used together with other
objc modules.
From https://gist.github.com/apettinen/5dc7bf1f6a07d148b2075725db6b1950
"""
import keyring
import objc
from ctypes import c_char
from Foundation import NSBundle
Security = NSBundle.bundleWithIdentifier_('com.apple.security')
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}'),
]
objc.loadBundleFunctions(Security, globals(), S_functions)
SecKeychainRef = objc.registerCFSignature(
'SecKeychainRef', b'^{OpaqueSecKeychainRef=}', SecKeychainGetTypeID()) # noqa: F821
SecKeychainItemRef = objc.registerCFSignature(
'SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID()) # noqa: F821
PassBuffRef = objc.createOpaquePointerType(
"PassBuffRef", b"^{OpaquePassBuff=}", None)
def resolve_password(password_length, password_buffer):
return (c_char * password_length).from_address(password_buffer.__pointer__)[:].decode('utf-8')
# Get the login keychain
result, login_keychain = SecKeychainOpen(b'login.keychain', None) # noqa: F821
class VortaDarwinKeyring(keyring.backend.KeyringBackend):
"""Homemade macOS Keychain Service"""
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
result, keychain_item = SecKeychainAddGenericPassword( # noqa: F821
login_keychain, len(service), service, len(repo_url), repo_url, len(password), password, None)
def get_password(self, service, repo_url):
result, password_length, password_buffer, keychain_item = SecKeychainFindGenericPassword( # noqa: F821
login_keychain, len(service), service.encode(), len(repo_url), repo_url.encode(), None, None, None)
password = None
if (result == 0) and (password_length != 0):
# We apparently were able to find a password
password = resolve_password(password_length, password_buffer)
return password
def delete_password(self, service, repo_url):
pass

32
src/vorta/keyring_db.py Normal file
View File

@ -0,0 +1,32 @@
import keyring
class VortaDBKeyring(keyring.backend.KeyringBackend):
"""
Our own fallback keyring service. Uses the main database
to store repo passwords if no other (more secure) backend
is available.
"""
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
from .models import RepoPassword
keyring_entry, created = RepoPassword.get_or_create(
url=repo_url,
defaults={'password': password}
)
keyring_entry.password = password
keyring_entry.save()
def get_password(self, service, repo_url):
from .models import RepoPassword
try:
keyring_entry = RepoPassword.get(url=repo_url)
return keyring_entry.password
except Exception:
return None
def delete_password(self, service, repo_url):
pass

View File

@ -18,39 +18,23 @@ from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
import subprocess
import keyring
from vorta.keyring_db import VortaDBKeyring
class VortaKeyring(keyring.backend.KeyringBackend):
"""Fallback keyring service."""
@classmethod
def priority(cls):
return 5
"""
Set the most appropriate Keyring backend for the current system.
def set_password(self, service, repo_url, password):
from .models import RepoPassword
keyring_entry, created = RepoPassword.get_or_create(
url=repo_url,
defaults={'password': password}
)
keyring_entry.password = password
keyring_entry.save()
For macOS we use our own implementation due to conflicts between
Keyring and the autostart code.
def get_password(self, service, repo_url):
from .models import RepoPassword
try:
keyring_entry = RepoPassword.get(url=repo_url)
return keyring_entry.password
except Exception:
return None
def delete_password(self, service, repo_url):
pass
# Select keyring/Workaround for pyinstaller+keyring issue.
For Linux not every system has SecretService available, so it will
fall back to a simple database keystore if needed.
"""
if sys.platform == 'darwin':
from keyring.backends import OS_X
keyring.set_keyring(OS_X.Keyring())
# from keyring.backends import OS_X
# keyring.set_keyring(OS_X.Keyring())
from vorta.keyring_darwin import VortaDarwinKeyring
keyring.set_keyring(VortaDarwinKeyring())
elif sys.platform == 'win32':
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
@ -60,9 +44,9 @@ elif sys.platform == 'linux':
SecretService.Keyring.priority() # Test if keyring works.
keyring.set_keyring(SecretService.Keyring())
except Exception:
keyring.set_keyring(VortaKeyring())
keyring.set_keyring(VortaDBKeyring())
else: # Fall back to saving password to database.
keyring.set_keyring(VortaKeyring())
keyring.set_keyring(VortaDBKeyring())
def nested_dict():
@ -222,22 +206,25 @@ def set_tray_icon(tray, active=False):
def open_app_at_startup(enabled=True):
"""
This function adds/removes the current app bundle from Login items in macOS
"""
if sys.platform == 'darwin':
print('Not implemented due to conflict with keyring package.')
# From https://stackoverflow.com/questions/26213884/cocoa-add-app-to-startup-in-sandbox-using-pyobjc
# from Foundation import NSDictionary
# from Cocoa import NSBundle, NSURL
# from CoreFoundation import kCFAllocatorDefault
# from LaunchServices import (LSSharedFileListCreate, kLSSharedFileListSessionLoginItems,
# LSSharedFileListInsertItemURL, kLSSharedFileListItemHidden,
# kLSSharedFileListItemLast, LSSharedFileListItemRemove)
#
# app_path = NSBundle.mainBundle().bundlePath()
# url = NSURL.alloc().initFileURLWithPath_(app_path)
# login_items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, None)
# props = NSDictionary.dictionaryWithObject_forKey_(True, kLSSharedFileListItemHidden)
#
# new_item = LSSharedFileListInsertItemURL(login_items, kLSSharedFileListItemLast,
# None, None, url, props, None)
# if not enabled:
# LSSharedFileListItemRemove(login_items, new_item)
from Foundation import NSDictionary
from Cocoa import NSBundle, NSURL
from CoreFoundation import kCFAllocatorDefault
# CF = CDLL(find_library('CoreFoundation'))
from LaunchServices import (LSSharedFileListCreate, kLSSharedFileListSessionLoginItems,
LSSharedFileListInsertItemURL, kLSSharedFileListItemHidden,
kLSSharedFileListItemLast, LSSharedFileListItemRemove)
app_path = NSBundle.mainBundle().bundlePath()
url = NSURL.alloc().initFileURLWithPath_(app_path)
login_items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, None)
props = NSDictionary.dictionaryWithObject_forKey_(True, kLSSharedFileListItemHidden)
new_item = LSSharedFileListInsertItemURL(login_items, kLSSharedFileListItemLast,
None, None, url, props, None)
if not enabled:
LSSharedFileListItemRemove(login_items, new_item)