Files
pyssh-client/app/dialogs/key_dialog.py
snoopy fbf7f580bc Retirer le pave de description du formulaire de generation de cle
Les info-bulles suffisent : le texte fixe sous le formulaire allongeait la
fenetre sans rien apporter de plus.

L'explication reste accessible de deux façons : en survolant une entree de la
liste deroulante, et en survolant le champ lui-meme, dont l'info-bulle reprend
desormais celle du type selectionne. Sans cette seconde info-bulle, l'explication
n'aurait ete visible qu'en ouvrant la liste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:20:02 +02:00

211 lines
7.9 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, KEY_TYPE_LABELS, KEY_TYPE_DESCRIPTIONS,
)
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()
for index, (key_type, label) in enumerate(KEY_TYPE_LABELS.items()):
self.type_combo.addItem(label, key_type)
# info-bulle par entree : visible en survolant la liste deroulante
self.type_combo.setItemData(
index, KEY_TYPE_DESCRIPTIONS.get(key_type, ""), Qt.ToolTipRole
)
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(self._on_type_changed)
self._on_type_changed()
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def _on_type_changed(self) -> None:
key_type = self.type_combo.currentData()
# l'info-bulle du champ reprend celle du type choisi : sans ca, l'explication
# ne serait visible qu'en ouvrant la liste deroulante
self.type_combo.setToolTip(KEY_TYPE_DESCRIPTIONS.get(key_type, ""))
# la taille en bits ne concerne que RSA
self.bits_spin.setEnabled(key_type == KEY_TYPE_RSA)
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()