def from_db(cls, profile, store_password=True, include_settings=True): profile_dict = model_to_dict(profile, exclude=[RepoModel.id]) # Have to retain profile ID keyring = VortaKeyring.get_keyring() if store_password: profile_dict['password'] = keyring.get_password('vorta-repo', profile.repo.url) # For all below, exclude ids to prevent collisions. DB will automatically reassign ids # Add SourceFileModel profile_dict['SourceFileModel'] = [ model_to_dict( source, recurse=False, exclude=[SourceFileModel.id]) for source in SourceFileModel.select().where( SourceFileModel.profile == profile)] # Add SchemaVersion profile_dict['SchemaVersion'] = model_to_dict(SchemaVersion.get(id=1)) if include_settings: # Add WifiSettingModel profile_dict['WifiSettingModel'] = [ model_to_dict( wifi, recurse=False) for wifi in WifiSettingModel.select().where( WifiSettingModel.profile == profile.id)] # Add SettingsModel profile_dict['SettingsModel'] = [ model_to_dict(s, exclude=[SettingsModel.id]) for s in SettingsModel] return ProfileExport(profile_dict)
def test_repo_add_success(qapp, qtbot, mocker, borg_json_output): # Add new repo window main = qapp.main_window main.repoTab.repoSelector.setCurrentIndex(1) add_repo_window = main.repoTab._window test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) qtbot.keyClicks(add_repo_window.passwordLineEdit, LONG_PASSWORD) qtbot.keyClicks(add_repo_window.confirmLineEdit, LONG_PASSWORD) stdout, stderr = borg_json_output('info') popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) add_repo_window.run() qtbot.waitUntil( lambda: EventLogModel.select().where(EventLogModel.returncode == 0). count() == 2, **pytest._wait_defaults) assert RepoModel.get(id=2).url == test_repo_url keyring = VortaKeyring.get_keyring() assert keyring.get_password("vorta-repo", RepoModel.get(id=2).url) == LONG_PASSWORD assert main.repoTab.repoSelector.currentText() == test_repo_url
def test_repo_add_success(qapp, qtbot, mocker, borg_json_output): # Add new repo window main = qapp.main_window main.repoTab.repoSelector.setCurrentIndex(1) add_repo_window = main.repoTab._window test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) qtbot.keyClicks(add_repo_window.passwordLineEdit, LONG_PASSWORD) qtbot.keyClicks(add_repo_window.confirmLineEdit, LONG_PASSWORD) stdout, stderr = borg_json_output('info') popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result) qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.LeftButton) with qtbot.waitSignal(add_repo_window.thread.result, timeout=3000) as _: pass assert EventLogModel.select().count() == 2 assert RepoModel.get(id=2).url == test_repo_url keyring = VortaKeyring.get_keyring() assert keyring.get_password("vorta-repo", RepoModel.get(id=2).url) == LONG_PASSWORD assert main.repoTab.repoSelector.currentText() == test_repo_url
def test_keyring(qapp): UNICODE_PW = 'kjalsdfüadsfäadsfß' REPO = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL keyring = VortaKeyring.get_keyring() keyring.set_password('vorta-repo', REPO, UNICODE_PW) assert keyring.get_password("vorta-repo", REPO) == UNICODE_PW
def set_password(self, URL): ''' Autofill password from keyring only if current entry is empty ''' password = VortaKeyring.get_keyring().get_password('vorta-repo', URL) if password and self.passwordLineEdit.text() == "": self.passwordLabel.setText(self.tr("Autofilled password from password manager.")) self.passwordLineEdit.setText(password) if self.__class__ == AddRepoWindow: self.confirmLineEdit.setText(password)
def test_password_autofill(qapp, qtbot): main = qapp.main_window main.repoTab.repoSelector.setCurrentIndex(1) add_repo_window = main.repoTab._window test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain keyring = VortaKeyring.get_keyring() password = str(uuid.uuid4()) keyring.set_password('vorta-repo', test_repo_url, password) qtbot.keyClicks(add_repo_window.repoURL, test_repo_url) assert(add_repo_window.passwordLineEdit.text() == password)
def display_password_backend(encryption): ''' Display password backend message based off current keyring ''' # flake8: noqa E501 if encryption != 'none': keyring = VortaKeyring.get_keyring() return trans_late( 'utils', "Storing the password in your password manager." ) if keyring.is_primary else trans_late( 'utils', 'Saving the password to disk. To store password more securely install a supported secret store such as KeepassXC' ) else: return ""
def init_repo_password_field(self, profile_export): """Try to prefill the borg passphrase either from the export or from the keyring.""" self.repoPassword.textChanged[str].connect(self.on_repo_password_changed) if profile_export.repo_password: self.repoPassword.setText(profile_export.repo_password) self.repoPassword.setDisabled(True) self.repoPassword.setToolTip(self.tr('The passphrase has been loaded from the export file')) elif profile_export.repo_url: keyring = VortaKeyring.get_keyring() repo_password = keyring.get_password('vorta-repo', profile_export.repo_url) if repo_password: self.repoPassword.setText(repo_password) self.repoPassword.setDisabled(True) self.repoPassword.setToolTip(self.tr('The passphrase has been loaded from your keyring'))
def __init__(self, profile): """ @type profile: BackupProfileModel """ super().__init__() self.profile = profile self.setupUi(self) self.setWindowTitle(self.tr("Export Profile")) self.buttonBox.accepted.connect(self.run) self.buttonBox.rejected.connect(self.reject) self.keyring = VortaKeyring.get_keyring() profile = self.profile if profile.repo is None or self.keyring.get_password( 'vorta-repo', profile.repo.url) is None: self.storePassword.setCheckState(False) self.storePassword.setDisabled(True) self.storePassword.setToolTip( self.tr('The current profile_export has no password'))
from collections import defaultdict from functools import reduce import operator import psutil from paramiko.rsakey import RSAKey from paramiko.ecdsakey import ECDSAKey from paramiko.ed25519key import Ed25519Key from paramiko import SSHException from PyQt5.QtWidgets import QFileDialog from PyQt5.QtGui import QIcon from PyQt5 import QtCore from vorta.keyring.abc import VortaKeyring from vorta.log import logger keyring = VortaKeyring.get_keyring() logger.info('Using %s Keyring implementation.', keyring.__class__.__name__) def nested_dict(): """ Combination of two idioms to quickly build dicts from lists of keys: - https://stackoverflow.com/a/16724937/3983708 - https://stackoverflow.com/a/14692747/3983708 """ return defaultdict(nested_dict) def get_dict_from_list(dataDict, mapList): return reduce(operator.getitem, mapList, dataDict)
def prepare(cls, profile): """ Prepare for running Borg. This function in the base class should be called from all subclasses and calls that define their own `cmd`. The `prepare()` step does these things: - validate if all conditions to run command are met - build borg command `prepare()` is run 2x. First at the global level and then for each subcommand. :return: dict(ok: book, message: str) """ ret = {'ok': False} if cls.prepare_bin() is None: ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret if profile.repo is None: ret['message'] = trans_late('messages', 'Add a backup repository first.') return ret if not borg_compat.check('JSON_LOG'): ret['message'] = trans_late('messages', 'Your Borg version is too old. >=1.1.0 is required.') return ret # Try to get password from chosen keyring backend. with keyring_lock: cls.keyring = VortaKeyring.get_keyring() logger.debug("Using %s keyring to store passwords.", cls.keyring.__class__.__name__) ret['password'] = cls.keyring.get_password('vorta-repo', profile.repo.url) # Check if keyring is locked if profile.repo.encryption != 'none' and not cls.keyring.is_unlocked: ret['message'] = trans_late('messages', 'Please unlock your system password manager or disable it under Misc') return ret # Try to fall back to DB Keyring, if we use the system keychain. if ret['password'] is None and cls.keyring.is_system: logger.debug('Password not found in primary keyring. Falling back to VortaDBKeyring.') ret['password'] = VortaDBKeyring().get_password('vorta-repo', profile.repo.url) # Give warning and continue if password is found there. if ret['password'] is not None: logger.warning('Found password in database, but secure storage was available. ' 'Consider re-adding the repo to use it.') # Password is required for encryption, cannot continue if ret['password'] is None and not isinstance(profile.repo, FakeRepo) and profile.repo.encryption != 'none': ret['message'] = trans_late( 'messages', "Your repo passphrase was stored in a password manager which is no longer available.\n" "Try unlinking and re-adding your repo.") return ret ret['ssh_key'] = profile.ssh_key ret['repo_id'] = profile.repo.id ret['repo_url'] = profile.repo.url ret['extra_borg_arguments'] = profile.repo.extra_borg_arguments ret['profile_name'] = profile.name ret['profile_id'] = profile.id ret['ok'] = True return ret
def display_backend_warning(self): '''Display password backend message based off current keyring''' if self.encryptionComboBox.currentData() != 'none': self.passwordLabel.setText( VortaKeyring.get_keyring().get_backend_warning())
def to_db(self, overwrite_profile=False, overwrite_settings=True): profile_schema = self._profile_dict['SchemaVersion']['version'] keyring = VortaKeyring.get_keyring() if SCHEMA_VERSION < profile_schema: raise VersionException() elif SCHEMA_VERSION > profile_schema: # Add model upgrading code here, only needed if not adding columns if profile_schema < 16: for sourcedir in self._profile_dict['SourceFileModel']: sourcedir['dir_files_count'] = -1 sourcedir['dir_size'] = -1 sourcedir['path_isdir'] = False existing_profile = None if overwrite_profile: existing_profile = BackupProfileModel.get_or_none(BackupProfileModel.name == self.name) if existing_profile: self._profile_dict['id'] = existing_profile.id if not overwrite_profile or not existing_profile: # Guarantee uniqueness of ids while BackupProfileModel.get_or_none(BackupProfileModel.id == self.id) is not None: self._profile_dict['id'] += 1 # Add suffix incase names are the same if BackupProfileModel.get_or_none(BackupProfileModel.name == self.name) is not None: suffix = 1 while BackupProfileModel.get_or_none(BackupProfileModel.name == f"{self.name}-{suffix}") is not None: suffix += 1 self._profile_dict['name'] = f"{self.name}-{suffix}" # Load existing repo or restore it if self._profile_dict['repo']: repo = RepoModel.get_or_none(RepoModel.url == self.repo_url) if repo is None: # Load repo from export repo = dict_to_model(RepoModel, self._profile_dict['repo']) repo.save(force_insert=True) self._profile_dict['repo'] = model_to_dict(repo) if self.repo_password: keyring.set_password('vorta-repo', self.repo_url, self.repo_password) del self._profile_dict['password'] # Delete and recreate the tables to clear them if overwrite_settings: db.drop_tables([SettingsModel, WifiSettingModel]) db.create_tables([SettingsModel, WifiSettingModel]) SettingsModel.insert_many(self._profile_dict['SettingsModel']).execute() WifiSettingModel.insert_many(self._profile_dict['WifiSettingModel']).execute() # Set the profile ids to be match new profile for source in self._profile_dict['SourceFileModel']: source['profile'] = self.id SourceFileModel.insert_many(self._profile_dict['SourceFileModel']).execute() # Delete added dictionaries to make it match BackupProfileModel del self._profile_dict['SettingsModel'] del self._profile_dict['SourceFileModel'] del self._profile_dict['WifiSettingModel'] del self._profile_dict['SchemaVersion'] # dict to profile new_profile = dict_to_model(BackupProfileModel, self._profile_dict) if overwrite_profile and existing_profile: force_insert = False else: force_insert = True new_profile.save(force_insert=force_insert) init_db() # rerun db init code to perform the same operations on the new as as on application boot return new_profile