Client SSH/SFTP personnel pour Windows
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>
This commit is contained in:
198
app/dialogs/key_dialog.py
Normal file
198
app/dialogs/key_dialog.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user