Update
This commit is contained in:
@@ -25,7 +25,7 @@ PI_OPTS := \
|
||||
--hidden-import cryptography.hazmat.primitives.asymmetric.x25519 \
|
||||
--collect-submodules PyQt6
|
||||
|
||||
.PHONY: all linux windows release install run run-admin icon clean help
|
||||
.PHONY: all linux windows release install install-gnome uninstall-gnome run run-admin icon reset-password clean help
|
||||
|
||||
# ── Cible par défaut ────────────────────────────────────────────────────────
|
||||
all: linux
|
||||
@@ -85,12 +85,57 @@ release: clean linux
|
||||
@mv $(DIST)/$(APP) $(DIST)/$(APP)-$(VERSION)-linux-x86_64
|
||||
@echo " 📦 Release → $(DIST)/$(APP)-$(VERSION)-linux-x86_64"
|
||||
|
||||
# ── Installation GNOME (utilisateur local, sans sudo) ────────────────────────
|
||||
INSTALL_BIN := $(HOME)/.local/bin
|
||||
INSTALL_ICON := $(HOME)/.local/share/icons/hicolor/256x256/apps
|
||||
INSTALL_APPS := $(HOME)/.local/share/applications
|
||||
|
||||
install-gnome: linux
|
||||
@echo ""
|
||||
@echo " 🐧 Installation de WGSecure pour GNOME…"
|
||||
@mkdir -p $(INSTALL_BIN) $(INSTALL_ICON) $(INSTALL_APPS)
|
||||
@cp $(DIST)/$(APP) $(INSTALL_BIN)/$(APP)
|
||||
@chmod +x $(INSTALL_BIN)/$(APP)
|
||||
@echo " ✅ Binaire → $(INSTALL_BIN)/$(APP)"
|
||||
@cp $(ICON) $(INSTALL_ICON)/$(APP).png
|
||||
@echo " ✅ Icône → $(INSTALL_ICON)/$(APP).png"
|
||||
@printf '[Desktop Entry]\nType=Application\nName=WGSecure\nComment=WireGuard GUI avec MFA TOTP\nExec=%s\nIcon=%s\nCategories=Network;Security;\nStartupWMClass=wgsecure\nTerminal=false\n' \
|
||||
"$(INSTALL_BIN)/$(APP)" "$(APP)" \
|
||||
> $(INSTALL_APPS)/$(APP).desktop
|
||||
@echo " ✅ Lanceur → $(INSTALL_APPS)/$(APP).desktop"
|
||||
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||
@update-desktop-database $(INSTALL_APPS) 2>/dev/null || true
|
||||
@echo ""
|
||||
@echo " 🎉 Installation terminée. WGSecure est disponible dans le menu GNOME."
|
||||
@echo ""
|
||||
|
||||
uninstall-gnome:
|
||||
@echo " 🗑️ Désinstallation de WGSecure…"
|
||||
@rm -f $(INSTALL_BIN)/$(APP)
|
||||
@rm -f $(INSTALL_ICON)/$(APP).png
|
||||
@rm -f $(INSTALL_APPS)/$(APP).desktop
|
||||
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||
@update-desktop-database $(INSTALL_APPS) 2>/dev/null || true
|
||||
@echo " ✅ WGSecure désinstallé"
|
||||
|
||||
# ── Dépendances Python ────────────────────────────────────────────────────────
|
||||
install:
|
||||
$(PYTHON) -m pip install -r requirements.txt
|
||||
$(PYTHON) -m pip install pyinstaller
|
||||
@echo " ✅ Dépendances installées"
|
||||
|
||||
# ── Réinitialisation du mot de passe administrateur ──────────────────────────
|
||||
reset-password:
|
||||
@echo ""
|
||||
@echo " ⚠️ Réinitialisation du mot de passe administrateur WGSecure…"
|
||||
@$(PYTHON) -c "\
|
||||
import sys; sys.path.insert(0,'.');\
|
||||
from app.core.config import Config;\
|
||||
cfg = Config();\
|
||||
cfg.set_admin_password('');\
|
||||
print(' ✅ Mot de passe supprimé — accès admin sans restriction au prochain démarrage.')"
|
||||
@echo ""
|
||||
|
||||
# ── Lancer l'application ─────────────────────────────────────────────────────
|
||||
run:
|
||||
DISPLAY=:0 $(PYTHON) $(SRC)
|
||||
@@ -109,16 +154,19 @@ clean:
|
||||
# ── Aide ─────────────────────────────────────────────────────────────────────
|
||||
help:
|
||||
@echo ""
|
||||
@echo " ╔════════════════════════════════════════════╗"
|
||||
@echo " ╔══════════════════════════════════════════════╗"
|
||||
@echo " ║ 🛡️ WGSecure v$(VERSION) — Makefile ║"
|
||||
@echo " ╠════════════════════════════════════════════╣"
|
||||
@echo " ╠══════════════════════════════════════════════╣"
|
||||
@echo " ║ make install Installe les dépendances ║"
|
||||
@echo " ║ make linux Binaire Linux (onefile) ║"
|
||||
@echo " ║ make windows Binaire Windows (Wine) ║"
|
||||
@echo " ║ make release Linux + nommage release ║"
|
||||
@echo " ║ make setup-wine Python Windows dans Wine ║"
|
||||
@echo " ║ make install-gnome Installe dans GNOME ║"
|
||||
@echo " ║ make uninstall-gnome Désinstalle de GNOME ║"
|
||||
@echo " ║ make reset-password Supprime le MDP admin ║"
|
||||
@echo " ║ make run Lance l'application ║"
|
||||
@echo " ║ make run-admin Lance en mode admin ║"
|
||||
@echo " ║ make clean Supprime les artefacts ║"
|
||||
@echo " ╚════════════════════════════════════════════╝"
|
||||
@echo " ╚══════════════════════════════════════════════╝"
|
||||
@echo ""
|
||||
|
||||
@@ -18,10 +18,18 @@
|
||||
|---|---|---|
|
||||
| 🖥️ | **Interface systray** | Icône bouclier dans la barre de notification, connexion en un clic |
|
||||
| 👤 | **Mode User** (défaut) | Connexion / déconnexion WireGuard, statut RX/TX en temps réel |
|
||||
| 📊 | **Graphique bande passante** | Courbes RX/TX en direct sur les 30 dernières secondes |
|
||||
| 📋 | **Journal d'événements** | Horodatage de chaque connexion, déconnexion, erreur |
|
||||
| 🕘 | **Historique des sessions** | Durée, données transférées, serveur par session |
|
||||
| ⚙️ | **Mode Admin** | Configuration complète, protégé par mot de passe |
|
||||
| 🔑 | **Génération de clés** | Paire Curve25519 native (sans dépendance au binaire `wg`) |
|
||||
| 👤 | **Multi-profils** | Sauvegarde / bascule entre profils, import/export `.conf` |
|
||||
| 🛡️ | **Configuration WireGuard** | Serveur, port UDP, DNS, IP client, keepalive |
|
||||
| 🔐 | **MFA TOTP** | Compatible Google Authenticator / Aegis + QR Code |
|
||||
| 🔐 | **MFA TOTP** | Compatible Bitwarden + QR Code |
|
||||
| 🔒 | **Kill Switch** | Bloque tout trafic hors tunnel via iptables (Linux) |
|
||||
| 🔍 | **Test DNS leak** | Vérifie que le DNS passe bien par le tunnel |
|
||||
| 🏓 | **Ping en temps réel** | Badge de latence dans l'interface principale |
|
||||
| 🔄 | **Auto-reconnexion** | Surveillance et reconnexion automatique si le tunnel tombe |
|
||||
| 📡 | **Test de connexion** | Vérification UDP du serveur avant établissement du tunnel |
|
||||
| 🪟🐧 | **Cross-platform** | Windows 10+ et Linux (wg-quick) |
|
||||
|
||||
@@ -60,11 +68,15 @@ pip install -r requirements.txt
|
||||
### Mode User (par défaut)
|
||||
```bash
|
||||
python main.py
|
||||
# ou via Makefile :
|
||||
make run
|
||||
```
|
||||
|
||||
### ⚙️ Mode Administrateur (configuration)
|
||||
```bash
|
||||
python main.py --admin
|
||||
# ou via Makefile :
|
||||
make run-admin
|
||||
```
|
||||
|
||||
### Sans systray
|
||||
@@ -74,34 +86,90 @@ python main.py --no-tray
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Makefile
|
||||
|
||||
| Commande | Description |
|
||||
|---|---|
|
||||
| `make install` | Installe les dépendances Python |
|
||||
| `make linux` | Compile un binaire Linux autonome (PyInstaller) |
|
||||
| `make windows` | Compile un binaire Windows via Wine |
|
||||
| `make release` | Binaire Linux renommé avec la version |
|
||||
| `make install-gnome` | Installe dans le menu GNOME (`~/.local/`) |
|
||||
| `make uninstall-gnome` | Désinstalle du menu GNOME |
|
||||
| `make reset-password` | Supprime le mot de passe administrateur |
|
||||
| `make run` | Lance l'application |
|
||||
| `make run-admin` | Lance en mode administrateur |
|
||||
| `make clean` | Supprime les artefacts de build |
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Workflow recommandé
|
||||
|
||||
1. 🚀 Lancer `python main.py --admin`
|
||||
2. 🛡️ Onglet **WireGuard** : renseigner l'adresse, le port et la clé publique du serveur
|
||||
3. 🔑 Onglet **Clés** : générer une paire de clés → copier la clé publique sur le serveur
|
||||
4. 🔐 Onglet **MFA** : activer le MFA, générer un secret, scanner le QR Code
|
||||
4. 🔐 Onglet **MFA** : activer le MFA, générer un secret, scanner le QR Code avec Bitwarden
|
||||
5. 🔒 Onglet **Sécurité** : définir un mot de passe pour l'accès admin
|
||||
6. ✅ **Enregistrer** → quitter → lancer `python main.py` en mode user
|
||||
|
||||
---
|
||||
|
||||
## 🐧 Élévation de privilèges Linux
|
||||
|
||||
`wg-quick` nécessite les droits root pour monter/démonter le tunnel.
|
||||
WGSecure tente dans l'ordre :
|
||||
|
||||
1. **pkexec** (polkit) — dialogue graphique GNOME/KDE, aucune configuration requise
|
||||
2. **sudo -A** avec programme askpass graphique (`ssh-askpass`, etc.)
|
||||
3. **sudo -n** — fonctionne si une règle NOPASSWD est en place
|
||||
|
||||
### Supprimer la demande de mot de passe (optionnel)
|
||||
|
||||
Pour éviter toute invite à chaque connexion, créer une règle sudoers dédiée :
|
||||
|
||||
```bash
|
||||
sudo visudo -f /etc/sudoers.d/wgsecure
|
||||
```
|
||||
|
||||
Ajouter la ligne suivante (remplacer `<utilisateur>` par votre login) :
|
||||
|
||||
```
|
||||
<utilisateur> ALL=(ALL) NOPASSWD: /usr/bin/wg-quick
|
||||
```
|
||||
|
||||
> ⚠️ Cette règle accorde l'exécution de `wg-quick` sans mot de passe uniquement,
|
||||
> et non un accès root général.
|
||||
|
||||
### Réinitialiser le mot de passe admin oublié
|
||||
|
||||
```bash
|
||||
make reset-password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
WGSecure/
|
||||
├── main.py # 🚀 Point d'entrée
|
||||
├── requirements.txt
|
||||
├── Makefile
|
||||
├── app/
|
||||
│ ├── __init__.py # Version
|
||||
│ ├── core/
|
||||
│ │ ├── config.py # 💾 Configuration JSON persistante
|
||||
│ │ ├── wireguard.py # 🛡️ Clés, tunnel, statut
|
||||
│ │ ├── config.py # 💾 Configuration JSON persistante + bcrypt
|
||||
│ │ ├── wireguard.py # 🛡️ Clés, tunnel, statut, kill switch, DNS leak
|
||||
│ │ ├── history.py # 🕘 Historique des sessions (sessions.json)
|
||||
│ │ ├── log.py # 📋 Journal d'événements (events.json)
|
||||
│ │ └── mfa.py # 🔐 TOTP / QR Code
|
||||
│ ├── ui/
|
||||
│ │ ├── icons.py # 🎨 Icônes bouclier générées par QPainter
|
||||
│ │ ├── main_window.py # 👤 Fenêtre principale (mode user)
|
||||
│ │ ├── admin_window.py # ⚙️ Panneau admin (6 onglets, thème sombre)
|
||||
│ │ ├── mfa_dialog.py # 🔐 Dialog de vérification MFA
|
||||
│ │ ├── admin_window.py # ⚙️ Panneau admin (onglets, thème sombre)
|
||||
│ │ ├── bw_graph.py # 📊 Widget graphique bande passante
|
||||
│ │ ├── history_dialog.py # 🕘 Dialogue historique des sessions
|
||||
│ │ ├── mfa_dialog.py # 🔐 Dialogue de vérification MFA
|
||||
│ │ └── systray.py # 🖥️ Icône systray + menu contextuel
|
||||
│ └── utils/
|
||||
│ └── platform_utils.py # 🔧 Détection OS, élévation de privilèges
|
||||
@@ -113,25 +181,16 @@ WGSecure/
|
||||
|
||||
| Élément | Protection |
|
||||
|---|---|
|
||||
| 🔑 Mot de passe admin | Hash SHA-256 + salt aléatoire 16 octets |
|
||||
| 🔑 Mot de passe admin | **bcrypt** (rounds=12) + sel intégré — migration transparente depuis SHA-256 |
|
||||
| 🗝️ Clé privée WireGuard | `~/.wgsecure/config.json` (Linux) / `%APPDATA%\WGSecure\` (Windows) |
|
||||
| 🔐 Secret MFA TOTP | Stocké dans le fichier de configuration local |
|
||||
| 📄 Fichier `.conf` WireGuard | Permissions `0600` (lecture propriétaire uniquement) |
|
||||
| 🔒 Kill Switch | Chaîne iptables dédiée `WGS_KILLSWITCH` — isolée du pare-feu système |
|
||||
|
||||
> ⚠️ **Recommandation** : chiffrez votre disque pour protéger le fichier de configuration.
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Interface
|
||||
|
||||
| Fenêtre principale | Panneau Admin |
|
||||
|---|---|
|
||||
| Statut connexion en temps réel | 6 onglets thème sombre |
|
||||
| Bouton connexion / déconnexion | WireGuard · Clés · MFA · Test · Sécurité · À propos |
|
||||
| Icône systray bouclier colorée | Bandeau coloré par onglet |
|
||||
|
||||
---
|
||||
|
||||
## 👤 Auteur
|
||||
|
||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||
|
||||
+11
-8
@@ -2,6 +2,7 @@ import json
|
||||
import os
|
||||
import hashlib
|
||||
import secrets
|
||||
import bcrypt
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from app.utils.platform_utils import get_config_dir
|
||||
@@ -28,7 +29,7 @@ _DEFAULT: dict[str, Any] = {
|
||||
"admin_salt": "",
|
||||
"mfa_enabled": False,
|
||||
"mfa_secret": "",
|
||||
"active_profile": "Défaut",
|
||||
"active_profile": "H3",
|
||||
"profiles": {},
|
||||
"wg": deepcopy(_WG_DEFAULT),
|
||||
"ui": {
|
||||
@@ -94,19 +95,21 @@ class Config:
|
||||
self._data["admin_salt"] = ""
|
||||
self._data["admin_password_hash"] = ""
|
||||
else:
|
||||
salt = secrets.token_hex(16)
|
||||
hashed = hashlib.sha256((salt + password).encode()).hexdigest()
|
||||
self._data["admin_salt"] = salt
|
||||
self._data["admin_password_hash"] = hashed
|
||||
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12))
|
||||
self._data["admin_salt"] = ""
|
||||
self._data["admin_password_hash"] = hashed.decode("utf-8")
|
||||
self.save()
|
||||
|
||||
def check_admin_password(self, password: str) -> bool:
|
||||
salt = self._data.get("admin_salt", "")
|
||||
stored = self._data.get("admin_password_hash", "")
|
||||
if not stored:
|
||||
return True
|
||||
# Migration transparente : anciens hashes SHA-256 (64 hex chars, pas de $2b$)
|
||||
if not stored.startswith("$2"):
|
||||
salt = self._data.get("admin_salt", "")
|
||||
candidate = hashlib.sha256((salt + password).encode()).hexdigest()
|
||||
return secrets.compare_digest(candidate, stored)
|
||||
return bcrypt.checkpw(password.encode("utf-8"), stored.encode("utf-8"))
|
||||
|
||||
def has_admin_password(self) -> bool:
|
||||
return bool(self._data.get("admin_password_hash", ""))
|
||||
@@ -144,7 +147,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def active_profile(self) -> str:
|
||||
return self._data.get("active_profile", "Défaut")
|
||||
return self._data.get("active_profile", "H3")
|
||||
|
||||
def list_profiles(self) -> list[str]:
|
||||
return list(self._data.get("profiles", {}).keys())
|
||||
@@ -171,7 +174,7 @@ class Config:
|
||||
return False
|
||||
del profiles[name]
|
||||
if self._data.get("active_profile") == name:
|
||||
self._data["active_profile"] = "Défaut"
|
||||
self._data["active_profile"] = "H3"
|
||||
self.save()
|
||||
return True
|
||||
|
||||
|
||||
+24
-5
@@ -75,7 +75,7 @@ class AdminWindow(QDialog):
|
||||
| Qt.WindowType.WindowTitleHint
|
||||
| Qt.WindowType.WindowCloseButtonHint
|
||||
)
|
||||
self.setFixedSize(700, 560)
|
||||
self.setFixedSize(700, 600)
|
||||
self._build_ui()
|
||||
self._load_values()
|
||||
|
||||
@@ -340,11 +340,11 @@ class AdminWindow(QDialog):
|
||||
self._row(form, "", btn_gen_secret)
|
||||
lay.addWidget(grp)
|
||||
|
||||
grp2 = QGroupBox("QR Code — Scanner avec Google Authenticator / Aegis")
|
||||
grp2 = QGroupBox("QR Code — Scanner avec Bitwarden")
|
||||
v2 = QVBoxLayout(grp2)
|
||||
self._qr_label = QLabel("(générez un secret pour afficher le QR Code)")
|
||||
self._qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._qr_label.setFixedHeight(180)
|
||||
self._qr_label.setFixedHeight(230)
|
||||
v2.addWidget(self._qr_label)
|
||||
self._mfa_uri_label = QLabel("")
|
||||
self._mfa_uri_label.setWordWrap(True)
|
||||
@@ -736,6 +736,8 @@ class AdminWindow(QDialog):
|
||||
# Actions
|
||||
# ------------------------------------------------------------------ #
|
||||
def _import_conf(self):
|
||||
from PyQt6.QtWidgets import QInputDialog
|
||||
import os as _os
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Importer un fichier WireGuard",
|
||||
"", "WireGuard Config (*.conf);;Tous les fichiers (*)"
|
||||
@@ -746,6 +748,19 @@ class AdminWindow(QDialog):
|
||||
if not values:
|
||||
QMessageBox.warning(self, "Erreur", "Impossible de lire ce fichier .conf")
|
||||
return
|
||||
|
||||
# Demander le nom du profil (défaut = nom du fichier sans extension)
|
||||
default_name = _os.path.splitext(_os.path.basename(path))[0]
|
||||
name, ok = QInputDialog.getText(
|
||||
self, "Nom du profil",
|
||||
"Nom du nouveau profil :",
|
||||
text=default_name,
|
||||
)
|
||||
if not ok or not name.strip():
|
||||
return
|
||||
name = name.strip()
|
||||
|
||||
# Appliquer les valeurs dans les champs
|
||||
mapping = {
|
||||
"server_endpoint": self._srv_endpoint,
|
||||
"server_public_key": self._srv_pubkey,
|
||||
@@ -762,10 +777,14 @@ class AdminWindow(QDialog):
|
||||
self._srv_port.setValue(int(values["server_port"]))
|
||||
if "keepalive" in values:
|
||||
self._keepalive.setValue(int(values["keepalive"]))
|
||||
|
||||
# Sauvegarder en tant que nouveau profil
|
||||
self._save_values()
|
||||
self._cfg.save_profile(name)
|
||||
self._refresh_profile_list()
|
||||
QMessageBox.information(
|
||||
self, "Import réussi",
|
||||
f"Configuration importée depuis :\n{path}"
|
||||
f"Profil « {name} » créé depuis :\n{path}"
|
||||
)
|
||||
|
||||
def _export_conf(self):
|
||||
@@ -810,7 +829,7 @@ class AdminWindow(QDialog):
|
||||
return
|
||||
pixmap = mfa_core.generate_qr_pixmap(secret)
|
||||
self._qr_label.setPixmap(
|
||||
pixmap.scaled(160, 160, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
pixmap.scaled(210, 210, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
)
|
||||
self._mfa_uri_label.setText(mfa_core.get_provisioning_uri(secret))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget,
|
||||
QTableWidgetItem, QPushButton, QLabel, QHeaderView,
|
||||
QMessageBox,
|
||||
QMessageBox, QWidget,
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
|
||||
@@ -326,14 +326,14 @@ class MainWindow(QMainWindow):
|
||||
def _reload_profiles(self):
|
||||
self._profile_combo.blockSignals(True)
|
||||
self._profile_combo.clear()
|
||||
self._profile_combo.addItem("⭐ Défaut (actif)")
|
||||
self._profile_combo.addItem(f"⭐ {self._cfg.active_profile} (actif)")
|
||||
for name in self._cfg.list_profiles():
|
||||
marker = "✓ " if name == self._cfg.active_profile else " "
|
||||
self._profile_combo.addItem(f"{marker}{name}")
|
||||
self._profile_combo.blockSignals(False)
|
||||
|
||||
def _on_profile_changed(self, text: str):
|
||||
if "Défaut" in text:
|
||||
if "(actif)" in text:
|
||||
return
|
||||
name = text.lstrip("✓ ").strip()
|
||||
if not name or name == self._cfg.active_profile:
|
||||
|
||||
+25
-14
@@ -6,6 +6,9 @@ from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QFont
|
||||
from app.core import mfa as mfa_core
|
||||
|
||||
_DARK = "#1c2833"
|
||||
_DARK2 = "#17202a"
|
||||
|
||||
|
||||
class MFADialog(QDialog):
|
||||
def __init__(self, secret: str, parent=None):
|
||||
@@ -15,6 +18,10 @@ class MFADialog(QDialog):
|
||||
self.setWindowTitle("WGSecure — Authentification MFA")
|
||||
self.setFixedWidth(340)
|
||||
self.setModal(True)
|
||||
self.setStyleSheet(f"""
|
||||
QDialog {{ background: {_DARK2}; }}
|
||||
QLabel {{ color: white; background: transparent; }}
|
||||
""")
|
||||
self._build_ui()
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self._tick)
|
||||
@@ -37,12 +44,12 @@ class MFADialog(QDialog):
|
||||
sub = QLabel("Entrez le code à 6 chiffres de votre application d'authentification.")
|
||||
sub.setWordWrap(True)
|
||||
sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
sub.setStyleSheet("color: #555;")
|
||||
sub.setStyleSheet("color: rgba(255,255,255,0.6);")
|
||||
layout.addWidget(sub)
|
||||
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setStyleSheet("color: #ddd;")
|
||||
sep.setStyleSheet("color: rgba(255,255,255,0.15);")
|
||||
layout.addWidget(sep)
|
||||
|
||||
self._code_input = QLineEdit()
|
||||
@@ -53,14 +60,15 @@ class MFADialog(QDialog):
|
||||
f2.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 4)
|
||||
self._code_input.setFont(f2)
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #3498db; border-radius: 6px;"
|
||||
"QLineEdit { padding: 8px; border: 2px solid #2471a3;"
|
||||
f" border-radius: 6px; background: {_DARK}; color: white; }}"
|
||||
)
|
||||
self._code_input.returnPressed.connect(self._verify)
|
||||
layout.addWidget(self._code_input)
|
||||
|
||||
self._error_label = QLabel("")
|
||||
self._error_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._error_label.setStyleSheet("color: #e74c3c; font-weight: bold;")
|
||||
self._error_label.setStyleSheet("color: #f1948a; font-weight: bold;")
|
||||
layout.addWidget(self._error_label)
|
||||
|
||||
self._progress = QProgressBar()
|
||||
@@ -68,30 +76,31 @@ class MFADialog(QDialog):
|
||||
self._progress.setTextVisible(False)
|
||||
self._progress.setFixedHeight(6)
|
||||
self._progress.setStyleSheet(
|
||||
"QProgressBar { border-radius: 3px; background: #ecf0f1; }"
|
||||
"QProgressBar::chunk { background: #3498db; border-radius: 3px; }"
|
||||
f"QProgressBar {{ border-radius: 3px; background: {_DARK}; }}"
|
||||
"QProgressBar::chunk { background: #5dade2; border-radius: 3px; }"
|
||||
)
|
||||
layout.addWidget(self._progress)
|
||||
|
||||
self._timer_label = QLabel("")
|
||||
self._timer_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._timer_label.setStyleSheet("color: #999; font-size: 11px;")
|
||||
self._timer_label.setStyleSheet("color: #5d6d7e; font-size: 11px;")
|
||||
layout.addWidget(self._timer_label)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_cancel = QPushButton("Annuler")
|
||||
btn_cancel.setStyleSheet(
|
||||
"QPushButton { padding: 8px 16px; border-radius: 5px; "
|
||||
"background: #ecf0f1; } QPushButton:hover { background: #bdc3c7; }"
|
||||
"QPushButton { padding: 8px 16px; border-radius: 5px; border: none;"
|
||||
f" background: #2e4057; color: white; }}"
|
||||
"QPushButton:hover { background: #3d5166; }"
|
||||
)
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
self._btn_ok = QPushButton("Vérifier")
|
||||
self._btn_ok.setDefault(True)
|
||||
self._btn_ok.setStyleSheet(
|
||||
"QPushButton { padding: 8px 20px; border-radius: 5px; "
|
||||
"background: #3498db; color: white; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #2980b9; }"
|
||||
"QPushButton { padding: 8px 20px; border-radius: 5px; border: none;"
|
||||
" background: #2471a3; color: white; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #1a5276; }"
|
||||
)
|
||||
self._btn_ok.clicked.connect(self._verify)
|
||||
btn_row.addWidget(btn_cancel)
|
||||
@@ -109,7 +118,8 @@ class MFADialog(QDialog):
|
||||
if len(raw) != 6 or not raw.isdigit():
|
||||
self._error_label.setText("Entrez exactement 6 chiffres.")
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #e74c3c; border-radius: 6px;"
|
||||
f"QLineEdit {{ padding: 8px; border: 2px solid #f1948a;"
|
||||
f" border-radius: 6px; background: {_DARK}; color: white; }}"
|
||||
)
|
||||
return
|
||||
if mfa_core.verify_code(self._secret, raw):
|
||||
@@ -119,7 +129,8 @@ class MFADialog(QDialog):
|
||||
self._error_label.setText("Code incorrect. Réessayez.")
|
||||
self._code_input.clear()
|
||||
self._code_input.setStyleSheet(
|
||||
"padding: 8px; border: 2px solid #e74c3c; border-radius: 6px;"
|
||||
f"QLineEdit {{ padding: 8px; border: 2px solid #f1948a;"
|
||||
f" border-radius: 6px; background: {_DARK}; color: white; }}"
|
||||
)
|
||||
|
||||
def is_verified(self) -> bool:
|
||||
|
||||
@@ -56,17 +56,68 @@ def run_command(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]:
|
||||
return -1, "", str(e)
|
||||
|
||||
|
||||
def run_privileged(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]:
|
||||
def run_privileged(cmd: list[str], timeout: int = 60) -> tuple[int, str, str]:
|
||||
if is_windows():
|
||||
return run_command(cmd, timeout)
|
||||
if has_root_privileges():
|
||||
return run_command(cmd, timeout)
|
||||
# Tente pkexec puis sudo
|
||||
for elevator in ("pkexec", "sudo"):
|
||||
code, out, err = run_command([elevator] + cmd, timeout)
|
||||
|
||||
# 1. pkexec — dialogue graphique polkit (GNOME/KDE), timeout long pour la saisie
|
||||
code, out, err = run_command(["pkexec"] + cmd, timeout)
|
||||
if code != -1 or "introuvable" not in err:
|
||||
return code, out, err
|
||||
return -1, "", "Élévation de privilèges impossible"
|
||||
|
||||
# 2. sudo avec programme askpass graphique (pas de TTY dans une app Qt)
|
||||
askpass = _find_askpass()
|
||||
if askpass:
|
||||
env = os.environ.copy()
|
||||
env["SUDO_ASKPASS"] = askpass
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", "-A"] + cmd,
|
||||
capture_output=True, text=True,
|
||||
timeout=timeout, env=env,
|
||||
)
|
||||
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return -1, "", "Timeout élévation sudo"
|
||||
except Exception as e:
|
||||
return -1, "", str(e)
|
||||
|
||||
# 3. sudo classique (fonctionne si NOPASSWD configuré dans sudoers)
|
||||
code, out, err = run_command(["sudo", "-n"] + cmd, 10)
|
||||
if code != -1:
|
||||
return code, out, err
|
||||
|
||||
return (
|
||||
-1, "",
|
||||
"Élévation de privilèges impossible.\n"
|
||||
"Solutions :\n"
|
||||
" • Installer pkexec (polkit) pour le dialogue graphique\n"
|
||||
" • Ou ajouter dans /etc/sudoers :\n"
|
||||
f" {os.environ.get('USER','<user>')} ALL=(ALL) NOPASSWD: /usr/bin/wg-quick"
|
||||
)
|
||||
|
||||
|
||||
def _find_askpass() -> str:
|
||||
"""Cherche un programme askpass graphique disponible."""
|
||||
candidates = [
|
||||
os.environ.get("SUDO_ASKPASS", ""),
|
||||
"/usr/lib/openssh/gnome-ssh-askpass",
|
||||
"/usr/bin/ssh-askpass",
|
||||
"/usr/bin/x11-ssh-askpass",
|
||||
"/usr/libexec/ssh-askpass",
|
||||
]
|
||||
for p in candidates:
|
||||
if p and os.path.isfile(p) and os.access(p, os.X_OK):
|
||||
return p
|
||||
# Cherche aussi via shutil
|
||||
import shutil
|
||||
for name in ("ssh-askpass", "x11-ssh-askpass", "gnome-ssh-askpass"):
|
||||
found = shutil.which(name)
|
||||
if found:
|
||||
return found
|
||||
return ""
|
||||
|
||||
|
||||
def wg_available() -> bool:
|
||||
|
||||
@@ -3,3 +3,4 @@ cryptography>=41.0.0
|
||||
pyotp>=2.9.0
|
||||
qrcode>=7.4.2
|
||||
Pillow>=10.0.0
|
||||
bcrypt>=4.0.0
|
||||
|
||||
Reference in New Issue
Block a user