Files
pyssh-client/app/dialogs/key_dialog.py
snoopy 1760dc39ac Expliquer chaque type de cle dans le formulaire de generation
Choisir entre Ed25519, trois courbes NIST et RSA n'a de sens que si l'on sait
ce qu'elles impliquent. Chaque type dispose desormais d'un resume, affiche a la
fois en info-bulle dans la liste deroulante et sous le formulaire, mis a jour a
chaque changement de selection.

Chaque resume indique la courbe (ou l'absence de courbe pour RSA), l'equivalence
de robustesse avec RSA selon les recommandations du NIST (SP 800-57), la
compatibilite OpenSSH, et le cas d'usage : Ed25519 par defaut, ECDSA quand une
politique de conformite impose les courbes NIST, RSA pour du materiel ancien.
La reserve sur l'origine jamais expliquee des parametres des courbes NIST est
mentionnee, puisqu'elle motive le choix d'Ed25519 par defaut.

Le champ de taille en bits, qui ne concerne que RSA, est desormais active par la
meme fonction : un seul endroit gere la reaction au changement de type.

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

219 lines
8.2 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)
# Resume du type selectionne, toujours visible : plus utile qu'une info-bulle
# qu'il faut penser a aller chercher.
self.type_help = QLabel()
self.type_help.setObjectName("statusLabel")
self.type_help.setWordWrap(True)
self.type_help.setTextFormat(Qt.PlainText)
self.type_help.setMinimumHeight(90)
self.type_help.setAlignment(Qt.AlignTop)
layout.addWidget(self.type_help)
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()
self.type_help.setText(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()