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:
@@ -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
|
## [0.7.18] — 2026-09-05
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## ──────────────────────────────────────────────
|
## ──────────────────────────────────────────────
|
||||||
|
|
||||||
APP := wgsecure
|
APP := wgsecure
|
||||||
VERSION := 0.7.18
|
VERSION := 0.7.19
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
PYTHON := $(VENV)/bin/python3
|
PYTHON := $(VENV)/bin/python3
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🛡️ WGSecure (WGS)
|
# 🛡️ 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).
|
**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
|
## 👤 Auteur
|
||||||
|
|
||||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||||
Version :  — Septembre 2026
|
Version :  — Septembre 2026
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
__version__ = "0.7.18"
|
__version__ = "0.7.19"
|
||||||
APP_NAME = "WGSecure"
|
APP_NAME = "WGSecure"
|
||||||
APP_SHORT = "WGS"
|
APP_SHORT = "WGS"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.utils.platform_utils import (
|
|||||||
run_command,
|
run_command,
|
||||||
run_privileged,
|
run_privileged,
|
||||||
run_privileged_readonly,
|
run_privileged_readonly,
|
||||||
|
secure_delete,
|
||||||
wg_available,
|
wg_available,
|
||||||
wg_exe,
|
wg_exe,
|
||||||
wg_quick_available,
|
wg_quick_available,
|
||||||
@@ -367,6 +368,7 @@ def connect(cfg: Config) -> tuple[bool, str]:
|
|||||||
if dns_util.interface_exists(name):
|
if dns_util.interface_exists(name):
|
||||||
force_cleanup(cfg)
|
force_cleanup(cfg)
|
||||||
cmd = [wireguard_exe(), "/installtunnelservice", result]
|
cmd = [wireguard_exe(), "/installtunnelservice", result]
|
||||||
|
try:
|
||||||
code, _, err = _run_wireguard_service_cmd(cmd, timeout=90)
|
code, _, err = _run_wireguard_service_cmd(cmd, timeout=90)
|
||||||
# /installtunnelservice enregistre le service et rend la main : le
|
# /installtunnelservice enregistre le service et rend la main : le
|
||||||
# démarrage effectif du tunnel est asynchrone. Tester l'état
|
# démarrage effectif du tunnel est asynchrone. Tester l'état
|
||||||
@@ -375,6 +377,20 @@ def connect(cfg: Config) -> tuple[bool, str]:
|
|||||||
return True, _post_connect_message(cfg)
|
return True, _post_connect_message(cfg)
|
||||||
force_cleanup(cfg)
|
force_cleanup(cfg)
|
||||||
return False, err or f"Échec de l'installation du tunnel (code {code})"
|
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.
|
# Déjà monté et fonctionnel : rien à faire.
|
||||||
if is_connected(cfg):
|
if is_connected(cfg):
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ def _validate_config_path(config_path, name: str, handle) -> str:
|
|||||||
# appartient pas / ne lui est pas lisible. Fail closed — si
|
# appartient pas / ne lui est pas lisible. Fail closed — si
|
||||||
# l'usurpation elle-même échoue, on refuse.
|
# l'usurpation elle-même échoue, on refuse.
|
||||||
try:
|
try:
|
||||||
win32pipe.ImpersonateNamedPipeClient(handle)
|
win32security.ImpersonateNamedPipeClient(handle)
|
||||||
except pywintypes.error as e:
|
except pywintypes.error as e:
|
||||||
raise ValidationError(f"Identité de l'appelant non vérifiable : {e}") from None
|
raise ValidationError(f"Identité de l'appelant non vérifiable : {e}") from None
|
||||||
try:
|
try:
|
||||||
|
|||||||
+31
-1
@@ -15,7 +15,9 @@ from app.core import dns as dns_util
|
|||||||
from app.core import mfa as mfa_core
|
from app.core import mfa as mfa_core
|
||||||
from app.ui import theme
|
from app.ui import theme
|
||||||
from app.ui.worker import ValueWorker
|
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):
|
class AdminWindow(QDialog):
|
||||||
def __init__(self, config: Config, parent=None):
|
def __init__(self, config: Config, parent=None):
|
||||||
@@ -1404,6 +1406,34 @@ class AdminWindow(QDialog):
|
|||||||
"accidentelle — déverrouillez-les dans l'onglet Tunnel pour les "
|
"accidentelle — déverrouillez-les dans l'onglet Tunnel pour les "
|
||||||
"éditer."
|
"é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):
|
def _export_conf(self):
|
||||||
self._save_values()
|
self._save_values()
|
||||||
|
|||||||
@@ -22,6 +22,34 @@ def get_config_dir() -> str:
|
|||||||
return path
|
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:
|
def get_wg_config_dir() -> str:
|
||||||
if is_windows():
|
if is_windows():
|
||||||
base = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
|
base = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
; ──────────────────────────────────────────────
|
; ──────────────────────────────────────────────
|
||||||
|
|
||||||
#define MyAppName "WGSecure"
|
#define MyAppName "WGSecure"
|
||||||
#define MyAppVersion "0.7.18"
|
#define MyAppVersion "0.7.19"
|
||||||
#define MyAppPublisher "WGSecure"
|
#define MyAppPublisher "WGSecure"
|
||||||
#define MyAppExeName "wgsecure.exe"
|
#define MyAppExeName "wgsecure.exe"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user