Skip to content

ssh_tunnel_manager.gui.components.ssh_key_generator

ssh_key_generator

SSH Tunnel Manager - SSH Key Generator Simple SSH key generation and management for tunnels

Classes

SSHKeyGeneratorDialog

Bases: QDialog

Simple SSH key generator dialog.

Source code in src/ssh_tunnel_manager/gui/components/ssh_key_generator.py
class SSHKeyGeneratorDialog(QDialog):
    """Simple SSH key generator dialog."""

    def __init__(self, parent=None, tunnel_config=None):
        super().__init__(parent)
        self.tunnel_config = tunnel_config
        self.setWindowTitle("SSH Key Generator")
        self.setGeometry(200, 200, 600, 500)
        self.setModal(True)

        self._setup_ui()
        self._setup_connections()

    def _setup_ui(self):
        """Setup the user interface."""
        layout = QVBoxLayout(self)

        # Header info
        if self.tunnel_config:
            info_label = QLabel(f"Generate SSH key for tunnel: {self.tunnel_config.get('name', 'Unknown')}")
            info_label.setStyleSheet("font-weight: bold; color: #2196F3;")
            layout.addWidget(info_label)

        # Key generation settings
        key_group = QGroupBox("Key Generation Settings")
        key_layout = QFormLayout(key_group)

        self.key_type = QComboBox()
        self.key_type.addItems(["ed25519", "rsa", "ecdsa"])
        self.key_type.setCurrentText("ed25519")
        key_layout.addRow("Key Type:", self.key_type)

        self.key_bits = QSpinBox()
        self.key_bits.setRange(2048, 8192)
        self.key_bits.setValue(4096)
        self.key_bits.setEnabled(False)  # Only for RSA
        key_layout.addRow("Key Bits (RSA only):", self.key_bits)

        self.key_comment = QLineEdit()
        if self.tunnel_config:
            default_comment = f"SSH key for {self.tunnel_config.get('name', 'tunnel')}"
        else:
            default_comment = "SSH key generated by SSH Tunnel Manager"
        self.key_comment.setText(default_comment)
        key_layout.addRow("Comment:", self.key_comment)

        self.passphrase = QLineEdit()
        self.passphrase.setEchoMode(QLineEdit.Password)
        self.passphrase.setPlaceholderText("Optional passphrase for key protection")
        key_layout.addRow("Passphrase:", self.passphrase)

        layout.addWidget(key_group)

        # File paths
        path_group = QGroupBox("Key File Paths")
        path_layout = QFormLayout(path_group)

        self.private_key_path = QLineEdit()
        default_ssh_dir = os.path.expanduser("~/.ssh")
        if self.tunnel_config:
            key_name = f"id_{self.key_type.currentText()}_{self.tunnel_config.get('name', 'tunnel')}"
        else:
            key_name = f"id_{self.key_type.currentText()}"

        self.private_key_path.setText(os.path.join(default_ssh_dir, key_name))

        private_layout = QHBoxLayout()
        private_layout.addWidget(self.private_key_path)
        browse_private = QPushButton("Browse...")
        browse_private.clicked.connect(self._browse_private_key)
        private_layout.addWidget(browse_private)
        path_layout.addRow("Private Key:", private_layout)

        self.public_key_path = QLabel()
        self._update_public_key_path()
        path_layout.addRow("Public Key:", self.public_key_path)

        layout.addWidget(path_group)

        # Generate button
        self.generate_button = QPushButton("Generate SSH Key Pair")
        self.generate_button.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 8px; }")
        layout.addWidget(self.generate_button)

        # Results area
        results_group = QGroupBox("Generated Keys")
        results_layout = QVBoxLayout(results_group)

        self.results_text = QTextEdit()
        self.results_text.setReadOnly(True)
        self.results_text.setFont(QFont("Consolas", 9))
        self.results_text.setMaximumHeight(200)
        results_layout.addWidget(self.results_text)

        # Action buttons for keys
        key_buttons_layout = QHBoxLayout()
        self.copy_public_button = QPushButton("Copy Public Key")
        self.copy_public_button.setEnabled(False)
        self.view_private_button = QPushButton("Show Private Key")
        self.view_private_button.setEnabled(False)
        self.deploy_key_button = QPushButton("Deploy Key to Server")
        self.deploy_key_button.setEnabled(False)
        self.open_ssh_dir_button = QPushButton("Open SSH Directory")

        key_buttons_layout.addWidget(self.copy_public_button)
        key_buttons_layout.addWidget(self.view_private_button)
        key_buttons_layout.addWidget(self.deploy_key_button)
        key_buttons_layout.addWidget(self.open_ssh_dir_button)
        key_buttons_layout.addStretch()

        results_layout.addLayout(key_buttons_layout)
        layout.addWidget(results_group)

        # Dialog buttons
        dialog_buttons = QHBoxLayout()
        self.close_button = QPushButton("Close")
        dialog_buttons.addStretch()
        dialog_buttons.addWidget(self.close_button)

        layout.addLayout(dialog_buttons)

    def _setup_connections(self):
        """Setup signal connections."""
        self.generate_button.clicked.connect(self._generate_key)
        self.close_button.clicked.connect(self.close)
        self.copy_public_button.clicked.connect(self._copy_public_key)
        self.view_private_button.clicked.connect(self._show_private_key)
        self.deploy_key_button.clicked.connect(self._deploy_key)
        self.open_ssh_dir_button.clicked.connect(self._open_ssh_directory)

        # Update paths when key type changes
        self.key_type.currentTextChanged.connect(self._on_key_type_changed)
        self.private_key_path.textChanged.connect(self._update_public_key_path)

    def _on_key_type_changed(self):
        """Handle key type change."""
        key_type = self.key_type.currentText()

        # Enable/disable key bits for RSA
        self.key_bits.setEnabled(key_type == "rsa")

        # Update default path
        private_path = self.private_key_path.text()
        if private_path:
            path_obj = Path(private_path)
            parent = path_obj.parent
            if self.tunnel_config:
                new_name = f"id_{key_type}_{self.tunnel_config.get('name', 'tunnel')}"
            else:
                new_name = f"id_{key_type}"

            self.private_key_path.setText(str(parent / new_name))

    def _update_public_key_path(self):
        """Update public key path based on private key path."""
        private_path = self.private_key_path.text()
        if private_path:
            self.public_key_path.setText(f"{private_path}.pub")
        else:
            self.public_key_path.setText("")

    def _browse_private_key(self):
        """Browse for private key file location."""
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Private Key As", self.private_key_path.text(), "All Files (*)"
        )
        if filename:
            self.private_key_path.setText(filename)

    def _generate_key(self):
        """Generate SSH key pair."""
        try:
            # Validate inputs
            private_path = self.private_key_path.text().strip()
            if not private_path:
                QMessageBox.warning(self, "Input Error", "Please specify a path for the private key.")
                return

            # Ensure SSH directory exists
            ssh_dir = os.path.dirname(private_path)
            os.makedirs(ssh_dir, exist_ok=True)

            # Check if file already exists
            if os.path.exists(private_path):
                reply = QMessageBox.question(
                    self, "File Exists", 
                    f"Key file already exists: {private_path}\n\nOverwrite?",
                    QMessageBox.Yes | QMessageBox.No
                )
                if reply == QMessageBox.No:
                    return

            # Build ssh-keygen command
            cmd = ["ssh-keygen"]
            cmd.extend(["-t", self.key_type.currentText()])

            if self.key_type.currentText() == "rsa":
                cmd.extend(["-b", str(self.key_bits.value())])

            cmd.extend(["-f", private_path])

            # Handle passphrase
            passphrase = self.passphrase.text()
            if passphrase:
                cmd.extend(["-N", passphrase])
            else:
                cmd.extend(["-N", ""])  # No passphrase

            # Add comment
            comment = self.key_comment.text().strip()
            if comment:
                cmd.extend(["-C", comment])

            # Execute ssh-keygen
            self.results_text.append("Generating SSH key pair...\n")
            self.results_text.append(f"Command: {' '.join(cmd[:-2])} -N [passphrase] -C '{comment}'\n")

            result = subprocess.run(cmd, capture_output=True, text=True)

            if result.returncode == 0:
                self.results_text.append("✅ SSH key pair generated successfully!\n")

                # Read and display public key
                public_path = f"{private_path}.pub"
                if os.path.exists(public_path):
                    with open(public_path, 'r') as f:
                        public_key = f.read().strip()

                    self.results_text.append("📋 Public Key:\n")
                    self.results_text.append(f"{public_key}\n\n")

                    self.results_text.append(f"🔐 Private Key: {private_path}\n")
                    self.results_text.append(f"🔑 Public Key: {public_path}\n\n")

                    self.results_text.append("📝 Next Steps:\n")
                    self.results_text.append("1. Copy the public key to your server's ~/.ssh/authorized_keys file\n")
                    self.results_text.append("2. Update your tunnel configuration to use the private key\n")
                    self.results_text.append("3. Test the connection\n")

                    # Enable action buttons
                    self.copy_public_button.setEnabled(True)
                    self.view_private_button.setEnabled(True)
                    self.deploy_key_button.setEnabled(True)

                    # Store paths for later use
                    self.generated_private_path = private_path
                    self.generated_public_path = public_path
                    self.generated_public_key = public_key

            else:
                self.results_text.append(f"❌ Error generating key: {result.stderr}\n")
                QMessageBox.critical(self, "Generation Error", f"Failed to generate SSH key:\n{result.stderr}")

        except FileNotFoundError:
            QMessageBox.critical(self, "SSH Error", "ssh-keygen command not found. Please install OpenSSH client.")
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Unexpected error: {str(e)}")

    def _copy_public_key(self):
        """Copy public key to clipboard."""
        if hasattr(self, 'generated_public_key'):
            clipboard = QApplication.clipboard()
            clipboard.setText(self.generated_public_key)
            QMessageBox.information(self, "Copied", "Public key copied to clipboard!")

    def _show_private_key(self):
        """Show private key (with warning)."""
        if not hasattr(self, 'generated_private_path'):
            return

        reply = QMessageBox.question(
            self, "Security Warning", 
            "⚠️ Showing private keys can be a security risk!\n\nAre you sure you want to display the private key?",
            QMessageBox.Yes | QMessageBox.No
        )

        if reply == QMessageBox.Yes:
            try:
                with open(self.generated_private_path, 'r') as f:
                    private_key = f.read()

                # Show in a new dialog
                dialog = QDialog(self)
                dialog.setWindowTitle("Private Key (Keep Secure!)")
                dialog.setGeometry(300, 300, 600, 400)

                layout = QVBoxLayout(dialog)

                warning = QLabel("⚠️ WARNING: Keep this private key secure! Never share it!")
                warning.setStyleSheet("color: red; font-weight: bold;")
                layout.addWidget(warning)

                text_edit = QTextEdit()
                text_edit.setReadOnly(True)
                text_edit.setFont(QFont("Consolas", 9))
                text_edit.setPlainText(private_key)
                layout.addWidget(text_edit)

                buttons = QHBoxLayout()
                copy_btn = QPushButton("Copy Private Key")
                copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(private_key))
                close_btn = QPushButton("Close")
                close_btn.clicked.connect(dialog.close)

                buttons.addWidget(copy_btn)
                buttons.addStretch()
                buttons.addWidget(close_btn)
                layout.addLayout(buttons)

                dialog.exec()

            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to read private key: {str(e)}")

    def _open_ssh_directory(self):
        """Open SSH directory in file explorer."""
        ssh_dir = os.path.expanduser("~/.ssh")
        if os.path.exists(ssh_dir):
            if os.name == 'nt':  # Windows
                os.startfile(ssh_dir)
            elif os.name == 'posix':  # Linux/macOS
                subprocess.run(['xdg-open', ssh_dir])
        else:
            QMessageBox.information(self, "Directory Not Found", f"SSH directory does not exist: {ssh_dir}")

    def _deploy_key(self):
        """Deploy the generated SSH key to the server."""
        if not hasattr(self, 'generated_public_path'):
            QMessageBox.warning(self, "No Key", "Please generate an SSH key first.")
            return

        # Import the deployment dialog
        from .ssh_key_deployment import SSHKeyDeploymentDialog

        # Create deployment dialog with current tunnel config and generated key
        dialog = SSHKeyDeploymentDialog(self, self.tunnel_config, self.generated_public_path)
        dialog.exec()

SSHKeyManager

Manager class for SSH key operations.

Source code in src/ssh_tunnel_manager/gui/components/ssh_key_generator.py
class SSHKeyManager:
    """Manager class for SSH key operations."""

    def __init__(self, parent=None):
        self.parent = parent

    def show_key_generator(self, tunnel_config=None):
        """Show SSH key generator dialog."""
        dialog = SSHKeyGeneratorDialog(self.parent, tunnel_config)
        dialog.exec()

    def get_available_keys(self) -> list:
        """Get list of available SSH keys."""
        ssh_dir = Path.home() / ".ssh"
        keys = []

        if ssh_dir.exists():
            for key_file in ssh_dir.glob("id_*"):
                if key_file.suffix != ".pub" and key_file.is_file():
                    # Check if corresponding public key exists
                    pub_file = key_file.with_suffix(key_file.suffix + ".pub")
                    if pub_file.exists():
                        keys.append({
                            'name': key_file.name,
                            'private_path': str(key_file),
                            'public_path': str(pub_file),
                            'type': self._detect_key_type(key_file)
                        })

        return keys

    def _detect_key_type(self, key_path: Path) -> str:
        """Detect SSH key type from file."""
        try:
            with open(key_path, 'r') as f:
                first_line = f.readline()
                if 'RSA' in first_line:
                    return 'rsa'
                elif 'ED25519' in first_line:
                    return 'ed25519'
                elif 'ECDSA' in first_line:
                    return 'ecdsa'
                else:
                    return 'unknown'
        except Exception:
            return 'unknown'
Functions
get_available_keys()

Get list of available SSH keys.

Source code in src/ssh_tunnel_manager/gui/components/ssh_key_generator.py
def get_available_keys(self) -> list:
    """Get list of available SSH keys."""
    ssh_dir = Path.home() / ".ssh"
    keys = []

    if ssh_dir.exists():
        for key_file in ssh_dir.glob("id_*"):
            if key_file.suffix != ".pub" and key_file.is_file():
                # Check if corresponding public key exists
                pub_file = key_file.with_suffix(key_file.suffix + ".pub")
                if pub_file.exists():
                    keys.append({
                        'name': key_file.name,
                        'private_path': str(key_file),
                        'public_path': str(pub_file),
                        'type': self._detect_key_type(key_file)
                    })

    return keys
show_key_generator(tunnel_config=None)

Show SSH key generator dialog.

Source code in src/ssh_tunnel_manager/gui/components/ssh_key_generator.py
def show_key_generator(self, tunnel_config=None):
    """Show SSH key generator dialog."""
    dialog = SSHKeyGeneratorDialog(self.parent, tunnel_config)
    dialog.exec()