From a2e9bf2886cb0561d4b1dfee79978203ede37ded Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 6 Dec 2018 17:41:22 +0800 Subject: [PATCH 1/3] Add own macOS Keychain implementation to avoid conflict with autostart. #81 --- requirements.d/conda.yml | 23 +++++ .../dev.txt | 0 setup.cfg | 1 + src/vorta/keyring_darwin.py | 61 ++++++++++++++ src/vorta/keyring_db.py | 32 +++++++ src/vorta/utils.py | 83 ++++++++----------- 6 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 requirements.d/conda.yml rename requirements-dev.txt => requirements.d/dev.txt (100%) create mode 100644 src/vorta/keyring_darwin.py create mode 100644 src/vorta/keyring_db.py diff --git a/requirements.d/conda.yml b/requirements.d/conda.yml new file mode 100644 index 00000000..ecbbd233 --- /dev/null +++ b/requirements.d/conda.yml @@ -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 + diff --git a/requirements-dev.txt b/requirements.d/dev.txt similarity index 100% rename from requirements-dev.txt rename to requirements.d/dev.txt diff --git a/setup.cfg b/setup.cfg index 9efab505..50ce0191 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/src/vorta/keyring_darwin.py b/src/vorta/keyring_darwin.py new file mode 100644 index 00000000..b2956b57 --- /dev/null +++ b/src/vorta/keyring_darwin.py @@ -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 diff --git a/src/vorta/keyring_db.py b/src/vorta/keyring_db.py new file mode 100644 index 00000000..0293571d --- /dev/null +++ b/src/vorta/keyring_db.py @@ -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 diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 02599675..ec864cbd 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -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) From 014f957c561810832d831a4c47970ee345c5e7c8 Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 6 Dec 2018 18:14:28 +0800 Subject: [PATCH 2/3] Resolve conflight with pytest-xdist and pyobjc. --- src/vorta/keyring_darwin.py | 89 ++++++++++++++++++++++--------------- src/vorta/updater.py | 4 +- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/vorta/keyring_darwin.py b/src/vorta/keyring_darwin.py index b2956b57..1c0646e1 100644 --- a/src/vorta/keyring_darwin.py +++ b/src/vorta/keyring_darwin.py @@ -1,61 +1,76 @@ +# flake8: noqa + """ 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 +Adapted 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) +from keyring.backend import KeyringBackend -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): +class VortaDarwinKeyring(KeyringBackend): """Homemade macOS Keychain Service""" + + login_keychain = None + + def _set_keychain(self): + """ + Lazy import to avoid conflict with pytest-xdist. + """ + import objc + 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()) + 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 + @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) + if not self.login_keychain: self._set_keychain() + + SecKeychainAddGenericPassword( + self.login_keychain, + len(service), service.encode(), + len(repo_url), repo_url.encode(), + len(password), password.encode(), + 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) + 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) password = None if (result == 0) and (password_length != 0): # We apparently were able to find a password - password = resolve_password(password_length, password_buffer) + password = _resolve_password(password_length, password_buffer) return password def delete_password(self, service, repo_url): pass + + +def _resolve_password(password_length, password_buffer): + from ctypes import c_char + return (c_char * password_length).from_address(password_buffer.__pointer__)[:].decode('utf-8') diff --git a/src/vorta/updater.py b/src/vorta/updater.py index 475a2373..5867918b 100644 --- a/src/vorta/updater.py +++ b/src/vorta/updater.py @@ -7,9 +7,9 @@ def get_updater(): if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # Use sparkle framework on macOS. # Examples: https://programtalk.com/python-examples/objc.loadBundle/ - from objc import loadBundle + import objc bundle_path = os.path.join(os.path.dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework') - loadBundle('Sparkle', globals(), bundle_path) + objc.loadBundle('Sparkle', globals(), bundle_path) sparkle = SUUpdater.sharedUpdater() # noqa: F821 if SettingsModel.get(key='updates_include_beta').value: sparkle.SharedUpdater.FeedURL = 'https://borgbase.github.io/vorta/appcast-pre.xml' From da3b2be3b42266ca8961751ee88e8d45ace500b0 Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 6 Dec 2018 18:24:30 +0800 Subject: [PATCH 3/3] Fix travis requirements.txt path --- .travis.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index eb5c4dba..2c0d7b4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,7 +67,7 @@ install: - pip install -U setuptools pip - pip install . - pip install borgbackup -- pip install -r requirements-dev.txt +- pip install -r requirements.d/dev.txt before_script: - if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm )& fi diff --git a/README.md b/README.md index b85dec1c..2bf6c534 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ $ vorta Install developer packages we use (pytest, tox, pyinstaller): ``` -pip install -r requirements-dev.txt +pip install -r requirements.d/dev.txt ``` Qt Creator is used to edit views. Install from [their site](https://www.qt.io/download) or using Homebrew and then open the .ui files in `vorta/UI`: