示例#1
0
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)
示例#2
0
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)
示例#3
0
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()
示例#4
0
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)