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()