Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added change passphrase dialog #1659

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/vorta/assets/UI/changeborgpass.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChangeBorgPassphrase</class>
<widget class="QDialog" name="ChangeRepositoryPass">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>466</width>
<height>274</height>
</rect>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="title">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Change Borg Passphrase</string>
</property>
<!-- Center align horizontally -->
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="0">
<!-- Single Box layout with three password inputs, current, new, confirm -->

<layout class="QFormLayout" name="repoDataFormLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>30</number>
</property>


<item row="1" column="0">
<widget class="QLabel" name="oldPasswordLabel">
<property name="text">
<string>Old Passphrase:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="oldPasswordLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>

<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>New Passphrase:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="passwordLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="confirmLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="confirmLabel">
<property name="text">
<string>Confirm Passphrase:</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="errorText">
<property name="text">
<string></string>
</property>
</widget>
</item>

</layout>


</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources />
<connections />
</ui>
7 changes: 7 additions & 0 deletions src/vorta/assets/UI/repotab.ui
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QToolButton" name="changePassbutton">
<property name="toolTip">
<string>Change Borg Passphrase</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="repoSelector">
<property name="sizePolicy">
Expand Down
1 change: 1 addition & 0 deletions src/vorta/assets/icons/lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/vorta/borg/_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'COMPACT_SUBCOMMAND': parse_version('1.2.0a1'),
'V122': parse_version('1.2.2'),
'V2': parse_version('2.0.0b1'),
'CHANGE_PASSPHRASE': parse_version('1.1.0'),
real-yfprojects marked this conversation as resolved.
Show resolved Hide resolved
# add new version-checks here.
}

Expand Down
59 changes: 59 additions & 0 deletions src/vorta/borg/change_passphrase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Any, Dict
from vorta.borg._compatibility import MIN_BORG_FOR_FEATURE
from vorta.config import LOG_DIR
from vorta.i18n import trans_late, translate
from vorta.utils import borg_compat
from .borg_job import BorgJob


class BorgChangePassJob(BorgJob):
def started_event(self):
self.app.backup_started_event.emit()
self.app.backup_progress_event.emit(self.tr('Changing Borg passphrase...'))

def finished_event(self, result: Dict[str, Any]):
"""
Process that the job terminated with the given results.

Parameters
----------
result : Dict[str, Any]
The (json-like) dictionary containing the job results.
"""
self.app.backup_finished_event.emit(result)
self.result.emit(result)
if result['returncode'] != 0:
self.app.backup_progress_event.emit(
translate(
'BorgChangePassJob',
'Errors during changing passphrase. See the <a href="{0}">logs</a> for details.',
).format(LOG_DIR.as_uri())
)
else:
self.app.backup_progress_event.emit(self.tr('Borg passphrase changed.'))

@classmethod
def prepare(cls, profile, oldPass, newPass):
ret = super().prepare(profile)
if not ret['ok']:
return ret
else:
ret['ok'] = False # Set back to false, so we can do our own checks here.

if not borg_compat.check('CHANGE_PASSPHRASE'):
ret['ok'] = False
ret['message'] = trans_late(
'messages', 'This feature needs Borg {} or higher.'.format(MIN_BORG_FOR_FEATURE['CHANGE_PASSPHRASE'])
)
return ret

cmd = ['borg', '--info', '--log-json', 'key', 'change-passphrase']
cmd.append(f'{profile.repo.url}')

ret['password'] = oldPass
real-yfprojects marked this conversation as resolved.
Show resolved Hide resolved
ret['additional_env'] = {'BORG_NEW_PASSPHRASE': newPass}

ret['ok'] = True
ret['cmd'] = cmd

return ret
111 changes: 111 additions & 0 deletions src/vorta/views/change_borg_passphrase_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from PyQt5 import QtCore, uic
from PyQt5.QtWidgets import QAction, QApplication, QDialogButtonBox, QLineEdit
from vorta.borg.change_passphrase import BorgChangePassJob
from vorta.i18n import translate
from vorta.utils import get_asset, validate_passwords
from vorta.views.utils import get_colored_icon

uifile = get_asset('UI/changeborgpass.ui')
ChangeBorgPassUI, ChangeBorgPassBase = uic.loadUiType(uifile)


class ChangeBorgPassphraseWindow(ChangeBorgPassBase, ChangeBorgPassUI):
change_borg_passphrase = QtCore.pyqtSignal(dict)

def __init__(self, profile):
super().__init__()
self.setupUi(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.result = None
self.profile = profile

# dialogButtonBox
self.saveButton = self.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
self.saveButton.setText(self.tr("Update"))

self.buttonBox.rejected.connect(self.close)
self.buttonBox.accepted.connect(self.run)
self.passwordLineEdit.textChanged.connect(self.password_listener)
self.confirmLineEdit.textChanged.connect(self.password_listener)

# Add clickable icon to toggle password visibility to end of box
self.showHideAction = QAction(self.tr("Show my passwords"), self)
self.showHideAction.setCheckable(True)
self.showHideAction.toggled.connect(self.set_visibility)

self.passwordLineEdit.addAction(self.showHideAction, QLineEdit.TrailingPosition)

self.set_icons()

def retranslateUi(self, dialog):
"""Retranslate strings in ui."""
super().retranslateUi(dialog)

# setupUi calls retranslateUi
if hasattr(self, 'saveButton'):
self.saveButton.setText(self.tr("Update"))

def set_icons(self):
self.showHideAction.setIcon(get_colored_icon("eye"))

def set_visibility(self, visible):
visibility = QLineEdit.Normal if visible else QLineEdit.Password
self.passwordLineEdit.setEchoMode(visibility)
self.confirmLineEdit.setEchoMode(visibility)

if visible:
self.showHideAction.setIcon(get_colored_icon("eye-slash"))
self.showHideAction.setText(self.tr("Hide my passwords"))
else:
self.showHideAction.setIcon(get_colored_icon("eye"))
self.showHideAction.setText(self.tr("Show my passwords"))

def run(self):
# if self.password_listener() and self.validate():
if self.password_listener():
oldPass = self.oldPasswordLineEdit.text()
newPass = self.passwordLineEdit.text()

params = BorgChangePassJob.prepare(self.profile, oldPass, newPass)
if params['ok']:
self.saveButton.setEnabled(False)
job = BorgChangePassJob(params['cmd'], params)
job.updated.connect(self._set_status)
job.result.connect(self.run_result)
QApplication.instance().jobs_manager.add_job(job)
else:
self._set_status(params['message'])

def _set_status(self, text):
self.errorText.setText(text)
self.errorText.repaint()

def run_result(self, result):
self.saveButton.setEnabled(True)
if result['returncode'] == 0:
self.change_borg_passphrase.emit(result)
self.accept()
else:
self._set_status(self.tr('Unable to change Borg passphrase.'))

def validate(self):
"""Check encryption type"""
if self.profile.repo.encryption.startswith('repokey'):
return True
self.errorText.setText(translate('utils', 'Encryption type must be repokey.'))
return False

def password_listener(self):
'''Validates passwords only if its going to be used'''
oldPass = self.oldPasswordLineEdit.text()
firstPass = self.passwordLineEdit.text()
secondPass = self.confirmLineEdit.text()

# Since borg originally does not have minimum character requirement
if len(oldPass) < 1:
self.errorText.setText(translate('utils', 'Old password is required.'))
return False

msg = validate_passwords(firstPass, secondPass)
self.errorText.setText(translate('utils', msg))
return not bool(msg)
19 changes: 19 additions & 0 deletions src/vorta/views/repo_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from PyQt5.QtWidgets import QApplication, QLayout, QMenu, QMessageBox
from vorta.store.models import ArchiveModel, BackupProfileMixin, RepoModel
from vorta.utils import borg_compat, get_asset, get_private_keys, pretty_bytes
from .change_borg_passphrase_dialog import ChangeBorgPassphraseWindow
from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow
from .ssh_dialog import SSHAddWindow
from .utils import get_colored_icon
Expand All @@ -25,6 +26,9 @@ def __init__(self, parent=None):
self.repoRemoveToolbutton.clicked.connect(self.repo_unlink_action)
self.copyURLbutton.clicked.connect(self.copy_URL_action)

# passphrase change button
self.changePassbutton.clicked.connect(self.change_borg_passphrase)

# init repo add button
self.menuAddRepo = QMenu(self.bAddRepo)

Expand Down Expand Up @@ -72,6 +76,7 @@ def set_icons(self):
self.repoRemoveToolbutton.setIcon(get_colored_icon('unlink'))
self.sshKeyToClipboardButton.setIcon(get_colored_icon('copy'))
self.copyURLbutton.setIcon(get_colored_icon('copy'))
self.changePassbutton.setIcon(get_colored_icon('lock'))

def set_repos(self):
self.repoSelector.clear()
Expand Down Expand Up @@ -106,6 +111,7 @@ def init_repo_stats(self):
# prepare translations
na = self.tr('N/A', "Not available.")
no_repo_selected = self.tr("Select a repository first.")
no_repokey_encryption = self.tr("Change Borg Passphrase (Disabled: No Repokey Encryption)")
jetchirag marked this conversation as resolved.
Show resolved Hide resolved
refresh = self.tr("Try refreshing the metadata of any archive.")

# set labels
Expand All @@ -124,6 +130,13 @@ def init_repo_stats(self):
self.sshComboBox.setEnabled(ssh_enabled)
self.sshKeyToClipboardButton.setEnabled(ssh_enabled)

# Disable the change passphrase button if encryption type is not repokey
if repo.encryption.startswith('repokey'):
self.changePassbutton.setEnabled(True)
else:
self.changePassbutton.setEnabled(False)
jetchirag marked this conversation as resolved.
Show resolved Hide resolved
self.changePassbutton.setToolTip(no_repokey_encryption)

# update stats
if repo.unique_csize is not None:
self.sizeCompressed.setText(pretty_bytes(repo.unique_csize))
Expand Down Expand Up @@ -248,6 +261,12 @@ def add_existing_repo(self):
# window.rejected.connect(lambda: self.repoSelector.setCurrentIndex(0))
window.open()

def change_borg_passphrase(self):
window = ChangeBorgPassphraseWindow(self.profile())
self._window = window # For tests
window.setParent(self, QtCore.Qt.Sheet)
window.open()

def repo_select_action(self):
profile = self.profile()
profile.repo = self.repoSelector.currentData()
Expand Down
2 changes: 2 additions & 0 deletions tests/borg_json_output/change_passphrase_stderr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type": "log_message", "time": 1679134475.3384268, "message": "Key updated", "levelname": "INFO", "name": "borg.archiver"}
{"type": "log_message", "time": 1679134475.338515, "message": "Key location: /Users/chirag/Projects/vorta/repo2", "levelname": "INFO", "name": "borg.archiver"}
Empty file.
Loading