"""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()