Reunit en un seul outil les fonctions utiles de MobaXterm, Termius et Bitvise, sans compte ni service tiers, avec toutes les donnees en local. - Terminaux multi-onglets : sessions SSH et terminaux Windows locaux (PowerShell / cmd) via la pseudo-console native ConPTY - Gestionnaire de fichiers SFTP facon WinSCP : double panneau, colonnes triables, glisser-deposer, renommage, permissions, file de transferts. Ouvrable sur une connexion en cours sans creer de favori - Gestion des cles SSH : generation Ed25519/RSA, import, export - Gestion du fichier known_hosts, avec remplacement guide d'une cle d'hote modifiee (cas frequent des serveurs en DHCP) - Trois themes (clair, bleu nuit, noir), commutables a chaud - Authentification automatique reprenant le comportement de la commande ssh (agent SSH et cles ~/.ssh), mot de passe demande seulement en secours Aucun mot de passe n'est stocke sur le disque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
"""Gestionnaire de cles SSH : generation, import, export."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
|
|
QMessageBox, QFileDialog, QFormLayout, QLineEdit, QComboBox, QSpinBox,
|
|
QDialogButtonBox, QApplication, QLabel,
|
|
)
|
|
|
|
from app.key_manager import KeyManager, KEY_TYPE_ED25519, KEY_TYPE_RSA
|
|
|
|
|
|
class GenerateKeyDialog(QDialog):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Generer une nouvelle cle SSH")
|
|
layout = QVBoxLayout(self)
|
|
form = QFormLayout()
|
|
|
|
self.name_edit = QLineEdit()
|
|
self.type_combo = QComboBox()
|
|
self.type_combo.addItem("Ed25519 (recommande)", KEY_TYPE_ED25519)
|
|
self.type_combo.addItem("RSA", KEY_TYPE_RSA)
|
|
self.bits_spin = QSpinBox()
|
|
self.bits_spin.setRange(2048, 8192)
|
|
self.bits_spin.setSingleStep(1024)
|
|
self.bits_spin.setValue(4096)
|
|
self.passphrase_edit = QLineEdit()
|
|
self.passphrase_edit.setEchoMode(QLineEdit.Password)
|
|
self.comment_edit = QLineEdit()
|
|
|
|
form.addRow("Nom :", self.name_edit)
|
|
form.addRow("Type :", self.type_combo)
|
|
form.addRow("Taille (RSA) :", self.bits_spin)
|
|
form.addRow("Passphrase (optionnel) :", self.passphrase_edit)
|
|
form.addRow("Commentaire :", self.comment_edit)
|
|
layout.addLayout(form)
|
|
|
|
self.type_combo.currentIndexChanged.connect(
|
|
lambda: self.bits_spin.setEnabled(self.type_combo.currentData() == KEY_TYPE_RSA)
|
|
)
|
|
self.bits_spin.setEnabled(False)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def values(self) -> dict:
|
|
return dict(
|
|
name=self.name_edit.text().strip() or "cle_ssh",
|
|
key_type=self.type_combo.currentData(),
|
|
rsa_bits=self.bits_spin.value(),
|
|
passphrase=self.passphrase_edit.text(),
|
|
comment=self.comment_edit.text().strip(),
|
|
)
|
|
|
|
|
|
class KeyManagerDialog(QDialog):
|
|
def __init__(self, key_manager: KeyManager, parent=None):
|
|
super().__init__(parent)
|
|
self.key_manager = key_manager
|
|
self.setWindowTitle("Gestionnaire de cles SSH")
|
|
self.setMinimumSize(560, 360)
|
|
|
|
layout = QVBoxLayout(self)
|
|
self.list = QListWidget()
|
|
layout.addWidget(self.list)
|
|
|
|
self.info_label = QLabel("")
|
|
self.info_label.setWordWrap(True)
|
|
layout.addWidget(self.info_label)
|
|
|
|
btn_row = QHBoxLayout()
|
|
self.generate_btn = QPushButton("Generer...")
|
|
self.import_btn = QPushButton("Importer...")
|
|
self.export_pub_btn = QPushButton("Exporter cle publique...")
|
|
self.export_priv_btn = QPushButton("Exporter cle privee...")
|
|
self.copy_pub_btn = QPushButton("Copier cle publique")
|
|
self.remove_btn = QPushButton("Supprimer")
|
|
for b in (self.generate_btn, self.import_btn, self.export_pub_btn,
|
|
self.export_priv_btn, self.copy_pub_btn, self.remove_btn):
|
|
btn_row.addWidget(b)
|
|
layout.addLayout(btn_row)
|
|
|
|
close_row = QHBoxLayout()
|
|
close_row.addStretch()
|
|
close_btn = QPushButton("Fermer")
|
|
close_btn.clicked.connect(self.accept)
|
|
close_row.addWidget(close_btn)
|
|
layout.addLayout(close_row)
|
|
|
|
self.generate_btn.clicked.connect(self.generate_key)
|
|
self.import_btn.clicked.connect(self.import_key)
|
|
self.export_pub_btn.clicked.connect(self.export_public)
|
|
self.export_priv_btn.clicked.connect(self.export_private)
|
|
self.copy_pub_btn.clicked.connect(self.copy_public)
|
|
self.remove_btn.clicked.connect(self.remove_key)
|
|
self.list.currentItemChanged.connect(self._update_info)
|
|
|
|
self.refresh()
|
|
|
|
def refresh(self) -> None:
|
|
self.list.clear()
|
|
for entry in self.key_manager.keys:
|
|
item = QListWidgetItem(f"{entry.name} [{entry.key_type}]")
|
|
item.setData(Qt.UserRole, entry.id)
|
|
self.list.addItem(item)
|
|
self.info_label.setText("")
|
|
|
|
def _current_entry(self):
|
|
item = self.list.currentItem()
|
|
if not item:
|
|
return None
|
|
key_id = item.data(Qt.UserRole)
|
|
return next((k for k in self.key_manager.keys if k.id == key_id), None)
|
|
|
|
def _update_info(self) -> None:
|
|
entry = self._current_entry()
|
|
if not entry:
|
|
self.info_label.setText("")
|
|
return
|
|
self.info_label.setText(
|
|
f"Chemin : {entry.private_path}\nEmpreinte : {entry.fingerprint}\n\n{entry.public_key_text}"
|
|
)
|
|
|
|
def generate_key(self) -> None:
|
|
dlg = GenerateKeyDialog(self)
|
|
if dlg.exec() != QDialog.Accepted:
|
|
return
|
|
vals = dlg.values()
|
|
try:
|
|
self.key_manager.generate_key(**vals)
|
|
except Exception as exc: # noqa: BLE001
|
|
QMessageBox.critical(self, "Erreur", str(exc))
|
|
return
|
|
self.refresh()
|
|
|
|
def import_key(self) -> None:
|
|
priv_path, _ = QFileDialog.getOpenFileName(self, "Selectionner la cle privee a importer")
|
|
if not priv_path:
|
|
return
|
|
name = Path(priv_path).stem
|
|
try:
|
|
self.key_manager.import_key(name=name, private_key_path=priv_path)
|
|
except Exception as exc: # noqa: BLE001
|
|
QMessageBox.critical(self, "Erreur", str(exc))
|
|
return
|
|
self.refresh()
|
|
|
|
def export_public(self) -> None:
|
|
entry = self._current_entry()
|
|
if not entry:
|
|
return
|
|
dest, _ = QFileDialog.getSaveFileName(self, "Exporter la cle publique", f"{entry.name}.pub")
|
|
if dest:
|
|
self.key_manager.export_public_key(entry, dest)
|
|
QMessageBox.information(self, "Export", "Cle publique exportee.")
|
|
|
|
def export_private(self) -> None:
|
|
entry = self._current_entry()
|
|
if not entry:
|
|
return
|
|
if QMessageBox.warning(
|
|
self, "Attention",
|
|
"La cle privee est sensible : quiconque y a acces peut se connecter a vos serveurs. Continuer ?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
) != QMessageBox.Yes:
|
|
return
|
|
dest, _ = QFileDialog.getSaveFileName(self, "Exporter la cle privee", entry.name)
|
|
if dest:
|
|
self.key_manager.export_private_key(entry, dest)
|
|
QMessageBox.information(self, "Export", "Cle privee exportee.")
|
|
|
|
def copy_public(self) -> None:
|
|
entry = self._current_entry()
|
|
if not entry:
|
|
return
|
|
QApplication.clipboard().setText(entry.public_key_text)
|
|
QMessageBox.information(self, "Copie", "Cle publique copiee dans le presse-papiers.")
|
|
|
|
def remove_key(self) -> None:
|
|
entry = self._current_entry()
|
|
if not entry:
|
|
return
|
|
delete_files = QMessageBox.question(
|
|
self, "Supprimer",
|
|
"Retirer aussi les fichiers de cle du disque (irreversible) ?\n"
|
|
"Non = seulement retirer de la liste.",
|
|
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
|
|
)
|
|
if delete_files == QMessageBox.Cancel:
|
|
return
|
|
self.key_manager.remove(entry.id, delete_files=(delete_files == QMessageBox.Yes))
|
|
self.refresh()
|