chore(release): v0.7.19

Sécurité : la clé privée ne subsiste plus en clair dans %APPDATA%.
Le .conf généré est écrasé puis supprimé dès que wireguard.exe l'a
consommé (il en garde sa propre copie chiffrée DPAPI), et l'import
propose d'effacer le fichier source. Windows uniquement : sous Linux
/etc/wireguard/<iface>.conf reste requis par wg-quick down.

Corrige aussi ImpersonateNamedPipeClient, qui appartient à
win32security et non à win32pipe (vérifié par introspection).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 08:36:16 +02:00
co-authored by Claude Sonnet 5
parent e96f829f8b
commit e272b9397e
9 changed files with 100 additions and 15 deletions
+11
View File
@@ -6,6 +6,17 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
---
## [0.7.19] — 2026-09-05
### Sécurité
- **La clé privée ne subsiste plus en clair dans `%APPDATA%\WGSecure`.** Le fichier `.conf` généré pour monter le tunnel y restait indéfiniment, lisible par l'utilisateur : il est désormais écrasé puis supprimé dès que `wireguard.exe` l'a consommé (celui-ci en conserve sa propre copie, chiffrée par DPAPI, dans `ProgramData\WireGuard\Configurations`, et la désinstallation du tunnel ne prend que le nom de l'interface). Windows uniquement : sous Linux, `/etc/wireguard/<iface>.conf` reste indispensable à `wg-quick down`.
- **Le `.conf` importé peut être effacé dans la foulée.** Après un import, l'application propose d'écraser et supprimer le fichier source, qui contient lui aussi la clé privée en clair — il traîne sinon dans Téléchargements ou sur le Bureau. Proposé et non imposé : c'est un fichier choisi par l'utilisateur, le détruire sans confirmation serait irrattrapable si l'import s'avérait incorrect.
### Corrigé
- **« Erreur interne : module win32pipe has no attribute ImpersonateNamedPipeClient » à la connexion.** Cette fonction appartient à `win32security`, pas à `win32pipe` (vérifié par introspection de pywin32, et non plus supposé).
---
## [0.7.18] — 2026-09-05
### Corrigé
+1 -1
View File
@@ -4,7 +4,7 @@
## ──────────────────────────────────────────────
APP := wgsecure
VERSION := 0.7.18
VERSION := 0.7.19
VENV := .venv
PYTHON := $(VENV)/bin/python3
PIP := $(VENV)/bin/pip
+2 -2
View File
@@ -1,6 +1,6 @@
# 🛡️ WGSecure (WGS)
🚀 v0.7.18 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard
🚀 v0.7.19 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard
**WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard avec une surcouche MFA (authentification à deux facteurs TOTP).
@@ -210,4 +210,4 @@ WGSecure/
## 👤 Auteur
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
Version : ![v0.7.18](https://img.shields.io/badge/v0.7.18-septembre%202026-2980b9?style=flat-square) — Septembre 2026
Version : ![v0.7.19](https://img.shields.io/badge/v0.7.19-septembre%202026-2980b9?style=flat-square) — Septembre 2026
+1 -1
View File
@@ -1,3 +1,3 @@
__version__ = "0.7.18"
__version__ = "0.7.19"
APP_NAME = "WGSecure"
APP_SHORT = "WGS"
+24 -8
View File
@@ -16,6 +16,7 @@ from app.utils.platform_utils import (
run_command,
run_privileged,
run_privileged_readonly,
secure_delete,
wg_available,
wg_exe,
wg_quick_available,
@@ -367,14 +368,29 @@ def connect(cfg: Config) -> tuple[bool, str]:
if dns_util.interface_exists(name):
force_cleanup(cfg)
cmd = [wireguard_exe(), "/installtunnelservice", result]
code, _, err = _run_wireguard_service_cmd(cmd, timeout=90)
# /installtunnelservice enregistre le service et rend la main : le
# démarrage effectif du tunnel est asynchrone. Tester l'état
# immédiatement conclurait « échec » sur un tunnel qui monte.
if code == 0 and _wait_connected(cfg, timeout=15):
return True, _post_connect_message(cfg)
force_cleanup(cfg)
return False, err or f"Échec de l'installation du tunnel (code {code})"
try:
code, _, err = _run_wireguard_service_cmd(cmd, timeout=90)
# /installtunnelservice enregistre le service et rend la main : le
# démarrage effectif du tunnel est asynchrone. Tester l'état
# immédiatement conclurait « échec » sur un tunnel qui monte.
if code == 0 and _wait_connected(cfg, timeout=15):
return True, _post_connect_message(cfg)
force_cleanup(cfg)
return False, err or f"Échec de l'installation du tunnel (code {code})"
finally:
# Ce .conf porte la clé privée en clair, dans un dossier lisible
# par l'utilisateur (%APPDATA%\WGSecure) : il ne doit pas y
# subsister une fois consommé. wireguard.exe en a déjà pris sa
# propre copie, chiffrée par DPAPI, dans son magasin
# (ProgramData\WireGuard\Configurations) — et
# /uninstalltunnelservice ne prend que le nom de l'interface,
# jamais un chemin : plus rien ne le relit ensuite. Il est
# réécrit à chaque connexion.
#
# Sous Linux au contraire, /etc/wireguard/<iface>.conf est
# indispensable à `wg-quick down` : on n'y touche pas (branche
# Windows uniquement).
secure_delete(result)
# Déjà monté et fonctionnel : rien à faire.
if is_connected(cfg):
+1 -1
View File
@@ -141,7 +141,7 @@ def _validate_config_path(config_path, name: str, handle) -> str:
# appartient pas / ne lui est pas lisible. Fail closed — si
# l'usurpation elle-même échoue, on refuse.
try:
win32pipe.ImpersonateNamedPipeClient(handle)
win32security.ImpersonateNamedPipeClient(handle)
except pywintypes.error as e:
raise ValidationError(f"Identité de l'appelant non vérifiable : {e}") from None
try:
+31 -1
View File
@@ -15,7 +15,9 @@ from app.core import dns as dns_util
from app.core import mfa as mfa_core
from app.ui import theme
from app.ui.worker import ValueWorker
from app.utils.platform_utils import is_windows, run_command, run_privileged
from app.utils.platform_utils import (
is_windows, run_command, run_privileged, secure_delete,
)
class AdminWindow(QDialog):
def __init__(self, config: Config, parent=None):
@@ -1404,6 +1406,34 @@ class AdminWindow(QDialog):
"accidentelle — déverrouillez-les dans l'onglet Tunnel pour les "
"éditer."
)
self._offer_source_shred(path)
def _offer_source_shred(self, path: str):
"""Propose d'effacer le .conf source une fois importé.
Ce fichier porte la clé privée en clair : le laisser traîner dans
Téléchargements ou sur le Bureau annule une bonne part de l'intérêt
de l'application. La suppression est proposée plutôt qu'imposée
c'est un fichier que l'utilisateur a choisi lui-même, et le détruire
sans demander serait irrattrapable si l'import s'avérait incorrect.
"""
reply = QMessageBox.question(
self, "Effacer le fichier source ?",
f"Le fichier importé contient votre clé privée en clair :\n{path}\n\n"
"Il n'est plus nécessaire : la configuration est enregistrée dans "
"le profil.\n\nL'effacer maintenant ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
if secure_delete(path):
QMessageBox.information(self, "Fichier effacé",
f"Fichier source supprimé :\n{path}")
else:
QMessageBox.warning(
self, "Suppression impossible",
f"Le fichier n'a pas pu être supprimé :\n{path}\n\n"
"Supprimez-le manuellement : il contient votre clé privée.")
def _export_conf(self):
self._save_values()
+28
View File
@@ -22,6 +22,34 @@ def get_config_dir() -> str:
return path
def secure_delete(path: str) -> bool:
"""Écrase puis supprime un fichier contenant un secret.
Sert aux fichiers `.conf` WireGuard, qui portent la clé privée en
clair : ils ne doivent pas subsister une fois consommés.
L'écrasement préalable est explicitement « au mieux » : sur SSD (wear
levelling, TRIM) ou sur un système de fichiers journalisé ou
copy-on-write, l'ancien contenu peut survivre dans des blocs que le
système n'expose plus. Il protège d'une récupération simple de
l'espace libre, pas d'une analyse forensique la seule protection
solide reste le chiffrement du disque.
"""
try:
size = os.path.getsize(path)
with open(path, "r+b") as f:
f.write(b"\0" * size)
f.flush()
os.fsync(f.fileno())
except OSError:
pass # fichier absent ou verrouillé : on tente quand même la suppression
try:
os.remove(path)
return True
except OSError:
return False
def get_wg_config_dir() -> str:
if is_windows():
base = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
+1 -1
View File
@@ -5,7 +5,7 @@
; ──────────────────────────────────────────────
#define MyAppName "WGSecure"
#define MyAppVersion "0.7.18"
#define MyAppVersion "0.7.19"
#define MyAppPublisher "WGSecure"
#define MyAppExeName "wgsecure.exe"