Le panneau des sessions etait declare Movable|Floatable mais pas Closable : il n'avait donc aucun bouton de fermeture et ne pouvait pas etre masque. Sa largeur plancher etait par ailleurs imposee par son contenu, ce qui empechait de le retrecir vraiment. - ajout de DockWidgetClosable, largeur minimale ramenee a 120 px - bascule par F9, par le menu Affichage, ou par la croix du panneau, les trois restant synchronises - separateur elargi a 6 px et surligne au survol, pour etre attrape facilement Generation de cles : ajout d'ECDSA sur les courbes NIST P-256, P-384 et P-521, en plus d'Ed25519 et RSA. Ed25519 reste le choix par defaut : c'est aussi une courbe elliptique (Curve25519), plus courte et plus rapide, et sans les interrogations qui entourent le choix des parametres des courbes NIST. Chaque type est verifie relisible par paramiko, donc utilisable pour se connecter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
7.7 KiB
Python
205 lines
7.7 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
|
|
|
|
|
|
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 key_type, label in KEY_TYPE_LABELS.items():
|
|
self.type_combo.addItem(label, key_type)
|
|
self.type_combo.setToolTip(
|
|
"Ed25519 : courbe elliptique moderne, courte et rapide — le choix par defaut.\n"
|
|
"ECDSA (P-256/384/521) : egalement sur courbe elliptique, utile si une politique\n"
|
|
"interne impose les courbes NIST.\n"
|
|
"RSA : le plus universel, a privilegier pour un serveur tres ancien."
|
|
)
|
|
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()
|