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>
219 lines
8.2 KiB
Python
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()
|