class TahoeConfigForm(QWidget): def __init__(self): super(TahoeConfigForm, self).__init__() self.rootcap = None self.settings = {} self.progress = None self.animation = None self.crypter = None self.crypter_thread = None self.connection_settings = ConnectionSettings() self.encoding_parameters = EncodingParameters() self.restore_selector = RestoreSelector(self) connection_settings_gbox = QGroupBox(self) connection_settings_gbox.setTitle("Connection settings:") connection_settings_gbox_layout = QGridLayout(connection_settings_gbox) connection_settings_gbox_layout.addWidget(self.connection_settings) encoding_parameters_gbox = QGroupBox(self) encoding_parameters_gbox.setTitle("Encoding parameters:") encoding_parameters_gbox_layout = QGridLayout(encoding_parameters_gbox) encoding_parameters_gbox_layout.addWidget(self.encoding_parameters) restore_selector_gbox = QGroupBox(self) restore_selector_gbox.setTitle("Import from Recovery Key:") restore_selector_gbox_layout = QGridLayout(restore_selector_gbox) restore_selector_gbox_layout.addWidget(self.restore_selector) self.buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) layout = QGridLayout(self) layout.addWidget(connection_settings_gbox) layout.addWidget(encoding_parameters_gbox) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding)) layout.addWidget(restore_selector_gbox) layout.addWidget(self.buttonbox) def set_name(self, name): self.connection_settings.name_line_edit.setText(name) def set_introducer(self, introducer): self.connection_settings.introducer_text_edit.setPlainText(introducer) def set_shares_total(self, shares): self.encoding_parameters.total_spinbox.setValue(int(shares)) def set_shares_needed(self, shares): self.encoding_parameters.needed_spinbox.setValue(int(shares)) def set_shares_happy(self, shares): self.encoding_parameters.happy_spinbox.setValue(int(shares)) def get_name(self): return self.connection_settings.name_line_edit.text().strip() def get_introducer(self): furl = self.connection_settings.introducer_text_edit.toPlainText() return furl.lower().strip() def get_shares_total(self): return self.encoding_parameters.total_spinbox.value() def get_shares_needed(self): return self.encoding_parameters.needed_spinbox.value() def get_shares_happy(self): return self.encoding_parameters.happy_spinbox.value() def reset(self): self.set_name('') self.set_introducer('') self.set_shares_total(1) self.set_shares_needed(1) self.set_shares_happy(1) self.rootcap = None def get_settings(self): return { 'nickname': self.get_name(), 'introducer': self.get_introducer(), 'shares-total': self.get_shares_total(), 'shares-needed': self.get_shares_needed(), 'shares-happy': self.get_shares_happy(), 'rootcap': self.rootcap # Maybe this should be user-settable? } def load_settings(self, settings_dict): for key, value in settings_dict.items(): if key == 'nickname': self.set_name(value) elif key == 'introducer': self.set_introducer(value) elif key == 'shares-total': self.set_shares_total(value) elif key == 'shares-needed': self.set_shares_total(value) elif key == 'shares-happy': self.set_shares_total(value) elif key == 'rootcap': self.rootcap = value def on_decryption_failed(self, msg): self.crypter_thread.quit() error(self, "Decryption failed", msg) self.crypter_thread.wait() def on_decryption_succeeded(self, plaintext): self.crypter_thread.quit() self.load_settings(json.loads(plaintext.decode('utf-8'))) self.crypter_thread.wait() def decrypt_content(self, data, password): self.progress = QProgressDialog("Trying to decrypt...", None, 0, 100) self.progress.show() self.animation = QPropertyAnimation(self.progress, b'value') self.animation.setDuration(5000) # XXX self.animation.setStartValue(0) self.animation.setEndValue(99) self.animation.start() self.crypter = Crypter(data, password.encode()) self.crypter_thread = QThread() self.crypter.moveToThread(self.crypter_thread) self.crypter.succeeded.connect(self.animation.stop) self.crypter.succeeded.connect(self.progress.close) self.crypter.succeeded.connect(self.on_decryption_succeeded) self.crypter.failed.connect(self.animation.stop) self.crypter.failed.connect(self.progress.close) self.crypter.failed.connect(self.on_decryption_failed) self.crypter_thread.started.connect(self.crypter.decrypt) self.crypter_thread.start() def parse_content(self, content): try: settings = json.loads(content.decode('utf-8')) except (UnicodeDecodeError, json.decoder.JSONDecodeError): password, ok = PasswordDialog.get_password( self, "Decryption passphrase (required):", "This Recovery Key is protected by a passphrase. Enter the " "correct passphrase to decrypt it.", show_stats=False) if ok: self.decrypt_content(content, password) return self.load_settings(settings) def load_from_file(self, path): try: with open(path, 'rb') as f: content = f.read() except Exception as e: # pylint: disable=broad-except error(self, type(e).__name__, str(e)) return self.parse_content(content)
class RecoveryKeyExporter(QObject): done = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.filepath = None self.progress = None self.animation = None self.crypter = None self.crypter_thread = None self.ciphertext = None def _on_encryption_failed(self, message): self.crypter_thread.quit() error(self.parent, "Error encrypting data", message) self.crypter_thread.wait() def _on_encryption_succeeded(self, ciphertext): self.crypter_thread.quit() if self.filepath: with atomic_write(self.filepath, mode="wb", overwrite=True) as f: f.write(ciphertext) self.done.emit(self.filepath) self.filepath = None else: self.ciphertext = ciphertext self.crypter_thread.wait() def _export_encrypted_recovery(self, gateway, password): settings = gateway.get_settings(include_rootcap=True) if gateway.use_tor: settings["hide-ip"] = True data = json.dumps(settings) self.progress = QProgressDialog("Encrypting...", None, 0, 100) self.progress.show() self.animation = QPropertyAnimation(self.progress, b"value") self.animation.setDuration(6000) # XXX self.animation.setStartValue(0) self.animation.setEndValue(99) self.animation.start() self.crypter = Crypter(data.encode(), password.encode()) self.crypter_thread = QThread() self.crypter.moveToThread(self.crypter_thread) self.crypter.succeeded.connect(self.animation.stop) self.crypter.succeeded.connect(self.progress.close) self.crypter.succeeded.connect(self._on_encryption_succeeded) self.crypter.failed.connect(self.animation.stop) self.crypter.failed.connect(self.progress.close) self.crypter.failed.connect(self._on_encryption_failed) self.crypter_thread.started.connect(self.crypter.encrypt) self.crypter_thread.start() dest, _ = QFileDialog.getSaveFileName( self.parent, "Select a destination", os.path.join( os.path.expanduser("~"), gateway.name + " Recovery Key.json.encrypted", ), ) if not dest: return if self.ciphertext: with atomic_write(dest, mode="wb", overwrite=True) as f: f.write(self.ciphertext) self.done.emit(dest) self.ciphertext = None else: self.filepath = dest def _export_plaintext_recovery(self, gateway): dest, _ = QFileDialog.getSaveFileName( self.parent, "Select a destination", os.path.join( os.path.expanduser("~"), gateway.name + " Recovery Key.json" ), ) if not dest: return try: gateway.export(dest, include_rootcap=True) except Exception as e: # pylint: disable=broad-except error(self.parent, "Error exporting Recovery Key", str(e)) return self.done.emit(dest) def do_export(self, gateway): password, ok = PasswordDialog.get_password( self.parent, "Encryption passphrase (optional):", "A long passphrase will help keep your files safe in the event " "that your Recovery Key is ever compromised.", ) if ok and password: self._export_encrypted_recovery(gateway, password) elif ok: self._export_plaintext_recovery(gateway)
class MainWindow(QMainWindow): def __init__(self, gui): super(MainWindow, self).__init__() self.gui = gui self.gateways = [] self.crypter = None self.crypter_thread = None self.export_data = None self.export_dest = None self.setWindowTitle(APP_NAME) self.setMinimumSize(QSize(500, 300)) self.shortcut_new = QShortcut(QKeySequence.New, self) self.shortcut_new.activated.connect(self.gui.show_setup_form) self.shortcut_open = QShortcut(QKeySequence.Open, self) self.shortcut_open.activated.connect(self.select_folder) self.shortcut_close = QShortcut(QKeySequence.Close, self) self.shortcut_close.activated.connect(self.close) self.shortcut_quit = QShortcut(QKeySequence.Quit, self) self.shortcut_quit.activated.connect(self.confirm_quit) self.combo_box = ComboBox() self.combo_box.activated[int].connect(self.on_grid_selected) self.central_widget = CentralWidget(self.gui) self.setCentralWidget(self.central_widget) invite_action = QAction(QIcon(resource('invite.png')), 'Enter an Invite Code...', self) invite_action.setStatusTip('Enter an Invite Code...') invite_action.triggered.connect(self.open_invite_receiver) folder_icon_default = QFileIconProvider().icon(QFileInfo(config_dir)) folder_icon_composite = CompositePixmap( folder_icon_default.pixmap(256, 256), resource('green-plus.png')) folder_icon = QIcon(folder_icon_composite) folder_action = QAction(folder_icon, "Add folder...", self) folder_action.setStatusTip("Add folder...") folder_from_local_action = QAction(QIcon(resource('laptop.png')), "From local computer...", self) folder_from_local_action.setStatusTip("Add folder from local computer") folder_from_local_action.setToolTip("Add folder from local computer") #self.from_local_action.setShortcut(QKeySequence.Open) folder_from_local_action.triggered.connect(self.select_folder) folder_from_invite_action = QAction(QIcon(resource('invite.png')), "From Invite Code...", self) folder_from_invite_action.setStatusTip("Add folder from Invite Code") folder_from_invite_action.setToolTip("Add folder from Invite Code") folder_from_invite_action.triggered.connect(self.open_invite_receiver) folder_menu = QMenu(self) folder_menu.addAction(folder_from_local_action) folder_menu.addAction(folder_from_invite_action) folder_button = QToolButton(self) folder_button.setDefaultAction(folder_action) folder_button.setMenu(folder_menu) folder_button.setPopupMode(2) folder_button.setStyleSheet( 'QToolButton::menu-indicator { image: none }') pair_action = QAction(QIcon(resource('laptop.png')), 'Connect another device...', self) pair_action.setStatusTip('Connect another device...') pair_action.triggered.connect(self.open_pair_widget) export_action = QAction(QIcon(resource('export.png')), 'Export Recovery Key', self) export_action.setStatusTip('Export Recovery Key...') export_action.setShortcut(QKeySequence.Save) export_action.triggered.connect(self.export_recovery_key) preferences_action = QAction(QIcon(resource('preferences.png')), 'Preferences', self) preferences_action.setStatusTip('Preferences') preferences_action.setShortcut(QKeySequence.Preferences) preferences_action.triggered.connect(self.toggle_preferences_widget) spacer_left = QWidget() spacer_left.setSizePolicy(QSizePolicy.Expanding, 0) spacer_right = QWidget() spacer_right.setSizePolicy(QSizePolicy.Expanding, 0) self.toolbar = self.addToolBar('') #self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) #self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toolbar.setIconSize(QSize(24, 24)) self.toolbar.setMovable(False) self.toolbar.addWidget(folder_button) #self.toolbar.addAction(invite_action) self.toolbar.addAction(pair_action) self.toolbar.addWidget(spacer_left) self.toolbar.addWidget(self.combo_box) self.toolbar.addWidget(spacer_right) self.toolbar.addAction(export_action) self.toolbar.addAction(preferences_action) self.status_bar = self.statusBar() self.status_bar_label = QLabel('Initializing...') self.status_bar.addPermanentWidget(self.status_bar_label) self.preferences_widget = PreferencesWidget() self.preferences_widget.accepted.connect(self.show_selected_grid_view) self.active_pair_widgets = [] self.active_invite_receivers = [] def populate(self, gateways): for gateway in gateways: if gateway not in self.gateways: self.gateways.append(gateway) self.combo_box.populate(self.gateways) self.central_widget.populate(self.gateways) self.central_widget.addWidget(self.preferences_widget) def current_view(self): return self.central_widget.currentWidget().layout().itemAt(0).widget() def select_folder(self): try: view = self.current_view() except AttributeError: return view.select_folder() def set_current_grid_status(self): if self.central_widget.currentWidget() == self.preferences_widget: return self.status_bar_label.setText(self.current_view().model().grid_status) self.gui.systray.update() def on_grid_selected(self, index): if index == self.combo_box.count() - 1: self.gui.show_setup_form() else: self.central_widget.setCurrentIndex(index) self.status_bar.show() self.set_current_grid_status() def show_selected_grid_view(self): for i in range(self.central_widget.count()): widget = self.central_widget.widget(i) try: gateway = widget.layout().itemAt(0).widget().gateway except AttributeError: continue if gateway == self.combo_box.currentData(): self.central_widget.setCurrentIndex(i) self.status_bar.show() self.set_current_grid_status() return self.combo_box.setCurrentIndex(0) # Fallback to 0 if none selected self.on_grid_selected(0) def show_error_msg(self, title, text): msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle(str(title)) msg.setText(str(text)) msg.exec_() def show_info_msg(self, title, text): msg = QMessageBox(self) msg.setIcon(QMessageBox.Information) msg.setWindowTitle(str(title)) msg.setText(str(text)) msg.exec_() def confirm_export(self, path): if os.path.isfile(path): # TODO: Confirm contents? self.show_info_msg( "Export successful", "Recovery Key successfully exported to {}".format(path)) else: self.show_error_msg( "Error exporting Recovery Key", "Destination file not found after export: {}".format(path)) def on_encryption_succeeded(self, ciphertext): self.crypter_thread.quit() if self.export_dest: with open(self.export_dest, 'wb') as f: f.write(ciphertext) self.confirm_export(self.export_dest) self.export_dest = None else: self.export_data = ciphertext self.crypter_thread.wait() def on_encryption_failed(self, message): self.crypter_thread.quit() self.show_error_msg("Error encrypting data", "Encryption failed: " + message) self.crypter_thread.wait() def export_encrypted_recovery(self, gateway, password): settings = gateway.get_settings(include_rootcap=True) data = json.dumps(settings) self.crypter = Crypter(data.encode(), password.encode()) self.crypter_thread = QThread() self.crypter.moveToThread(self.crypter_thread) self.crypter.succeeded.connect(self.on_encryption_succeeded) self.crypter.failed.connect(self.on_encryption_failed) self.crypter_thread.started.connect(self.crypter.encrypt) self.crypter_thread.start() dest, _ = QFileDialog.getSaveFileName( self, "Select a destination", os.path.join(os.path.expanduser('~'), gateway.name + ' Recovery Key.json.encrypted')) if not dest: return if self.export_data: with open(dest, 'wb') as f: f.write(self.export_data) self.confirm_export(dest) self.export_data = None else: self.export_dest = dest def export_plaintext_recovery(self, gateway): dest, _ = QFileDialog.getSaveFileName( self, "Select a destination", os.path.join(os.path.expanduser('~'), gateway.name + ' Recovery Key.json')) if not dest: return try: gateway.export(dest, include_rootcap=True) except Exception as e: # pylint: disable=broad-except self.show_error_msg("Error exporting Recovery Key", str(e)) return self.confirm_export(dest) def export_recovery_key(self): self.show_selected_grid_view() gateway = self.current_view().gateway password, ok = PasswordDialog.get_password( self, "Encryption passphrase (optional):") if ok and password: self.export_encrypted_recovery(gateway, password) elif ok: self.export_plaintext_recovery(gateway) def toggle_preferences_widget(self): if self.central_widget.currentWidget() == self.preferences_widget: self.show_selected_grid_view() else: self.status_bar.hide() for i in range(self.central_widget.count()): if self.central_widget.widget(i) == self.preferences_widget: self.central_widget.setCurrentIndex(i) def on_invite_received(self, _): for view in self.central_widget.views: view.model().monitor.scan_rootcap('star.png') def open_invite_receiver(self): invite_receiver = InviteReceiver(self.gui) invite_receiver.done.connect(self.on_invite_received) invite_receiver.closed.connect(self.active_invite_receivers.remove) invite_receiver.show() self.active_invite_receivers.append(invite_receiver) def open_pair_widget(self): gateway = self.combo_box.currentData() if gateway: pair_widget = ShareWidget(gateway, self.gui) pair_widget.closed.connect(self.active_pair_widgets.remove) pair_widget.show() self.active_pair_widgets.append(pair_widget) def confirm_quit(self): reply = QMessageBox.question( self, "Exit {}?".format(APP_NAME), "Are you sure you wish to quit? If you quit, {} will stop " "synchronizing your folders until you run it again.".format( APP_NAME), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: reactor.stop() def keyPressEvent(self, event): key = event.key() if key == Qt.Key_Escape and self.gui.systray.isSystemTrayAvailable(): self.hide() def closeEvent(self, event): if self.gui.systray.isSystemTrayAvailable(): event.accept() else: event.ignore() self.confirm_quit()
class RecoveryKeyImporter(QObject): done = pyqtSignal(dict) def __init__(self, parent=None): super().__init__() self.parent = parent self.filepath = None self.progress = None self.animation = None self.crypter = None self.crypter_thread = None def _on_decryption_failed(self, msg): logging.error("%s", msg) self.crypter_thread.quit() if msg == "Decryption failed. Ciphertext failed verification": msg = "The provided passphrase was incorrect. Please try again." reply = QMessageBox.critical( self.parent, "Decryption Error", msg, QMessageBox.Abort | QMessageBox.Retry, ) self.crypter_thread.wait() if reply == QMessageBox.Retry: self._load_from_file(self.filepath) def _on_decryption_succeeded(self, plaintext): logging.debug("Decryption of %s succeeded", self.filepath) self.crypter_thread.quit() try: settings = json.loads(plaintext.decode("utf-8")) except (UnicodeDecodeError, json.decoder.JSONDecodeError) as e: error(self, type(e).__name__, str(e)) return self.done.emit(settings) self.crypter_thread.wait() def _decrypt_content(self, data, password): logging.debug("Trying to decrypt %s...", self.filepath) self.progress = QProgressDialog( "Trying to decrypt {}...".format(os.path.basename(self.filepath)), None, 0, 100, ) self.progress.show() self.animation = QPropertyAnimation(self.progress, b"value") self.animation.setDuration(6000) # XXX self.animation.setStartValue(0) self.animation.setEndValue(99) self.animation.start() self.crypter = Crypter(data, password.encode()) self.crypter_thread = QThread() self.crypter.moveToThread(self.crypter_thread) self.crypter.succeeded.connect(self.animation.stop) self.crypter.succeeded.connect(self.progress.close) self.crypter.succeeded.connect(self._on_decryption_succeeded) self.crypter.failed.connect(self.animation.stop) self.crypter.failed.connect(self.progress.close) self.crypter.failed.connect(self._on_decryption_failed) self.crypter_thread.started.connect(self.crypter.decrypt) self.crypter_thread.start() def _parse_content(self, content): try: settings = json.loads(content.decode("utf-8")) except (UnicodeDecodeError, json.decoder.JSONDecodeError): logging.debug( "JSON decoding failed; %s is likely encrypted", self.filepath ) password, ok = PasswordDialog.get_password( self.parent, "Decryption passphrase (required):", "This Recovery Key is protected by a passphrase. Enter the " "correct passphrase to decrypt it.", show_stats=False, ) if ok: self._decrypt_content(content, password) return self.done.emit(settings) def _load_from_file(self, path): logging.debug("Loading %s...", self.filepath) try: with open(path, "rb") as f: content = f.read() except Exception as e: # pylint: disable=broad-except error(self, type(e).__name__, str(e)) return self._parse_content(content) def _select_file(self): dialog = QFileDialog(self.parent, "Select a Recovery Key") dialog.setDirectory(os.path.expanduser("~")) dialog.setFileMode(QFileDialog.ExistingFile) if dialog.exec_(): return dialog.selectedFiles()[0] return None def do_import(self, filepath=None): if not filepath: filepath = self._select_file() self.filepath = filepath if self.filepath: self._load_from_file(self.filepath)