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:
+1
-1
@@ -1,3 +1,3 @@
|
||||
__version__ = "0.7.18"
|
||||
__version__ = "0.7.19"
|
||||
APP_NAME = "WGSecure"
|
||||
APP_SHORT = "WGS"
|
||||
|
||||
+24
-8
@@ -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):
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user