Files
WGSecure/app/app_info.py
T
tuxgyverandClaude Opus 5 205258cbf1 feat(ui): trois sous-onglets de plus dans « À propos »
Mode d'emploi, Prérequis et Sécurité rejoignent Fonctions, Garanties,
Technique, Journal et Auteur.

Le contenu vit dans app_info.py, séparé de sa présentation : le relire
ou le corriger ne demande pas de traverser le code de l'interface. Deux
aides — _about_group_label et _about_grouped — découpent en groupes
titrés les listes d'une vingtaine de cartes, sans quoi Prérequis et
Sécurité se lisaient comme un seul bloc indifférencié.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:27:09 +02:00

407 lines
18 KiB
Python

"""Contenu de la page « À propos ».
Source de vérité unique pour les textes de la page : l'onglet Qt
(app/ui/admin_window.py, méthode `_tab_about`) ne fait que les mettre en
forme. La version, elle, vient de `app.__version__` (app/__init__.py), le
seul endroit où elle est bumpée à chaque release — jamais recopiée en dur ici.
"""
import os
import sys
from app import __version__
# En développement, CHANGELOG.md vit à la racine du dépôt, deux niveaux
# au-dessus de ce fichier. Une fois empaqueté par PyInstaller (--onefile),
# `__file__` pointe vers l'intérieur de l'exécutable et ce chemin n'existe
# plus : le fichier doit alors être cherché dans `sys._MEIPASS`, le dossier
# temporaire où l'exe extrait les données embarquées via `--add-data`.
if getattr(sys, "frozen", False):
_CHANGELOG_PATH = os.path.join(getattr(sys, "_MEIPASS", ""), "CHANGELOG.md")
else:
_CHANGELOG_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "CHANGELOG.md"
)
APP_NAME = "WGSecure"
TAGLINE = "Client WireGuard à accès authentifié par le serveur, pour Linux et Windows"
VERSION = __version__
LICENSE = "Usage personnel"
REPO_URL = "https://git.tuxtech.fr/tuxgyver/WGSecure"
AUTHOR_NAME = "Johnny"
AUTHOR_ROLE = "Auteur"
FEATURES = [
(
"Connexion en un clic",
"Icône dans la barre système, montage/démontage du tunnel WireGuard "
"sans quitter le systray.",
),
(
"Suivi en direct",
"Statut RX/TX, dernier handshake et latence affichés en continu ; "
"graphique de bande passante sur les 30 dernières secondes.",
),
(
"Journal et historique",
"Horodatage de chaque connexion, déconnexion et erreur ; historique "
"des sessions passées (durée, volume transféré, serveur).",
),
(
"Multi-profils",
"Plusieurs configurations WireGuard sauvegardées et basculables, "
"avec import/export de fichiers .conf standards. Les champs issus "
"d'un import se verrouillent pour éviter une divergence silencieuse "
"avec le pair déjà configuré côté serveur.",
),
(
"Génération de clés",
"Paire de clés Curve25519 générée nativement en Python, sans "
"dépendre du binaire `wg` pour cette étape.",
),
(
"Mode Admin protégé",
"Configuration complète (serveur, DNS, IP, keepalive, MTU) derrière "
"un mot de passe, et l'accès aux ressources distantes par un code "
"à 6 chiffres validé par le serveur.",
),
(
"Diagnostic réseau",
"Test de handshake réel sans monter le tunnel, détection du "
"filtrage sur port source, et test de fuite DNS.",
),
(
"Auto-reconnexion",
"Surveillance du tunnel et reconnexion automatique en cas de "
"coupure, à l'intervalle choisi.",
),
(
"Split-DNS",
"Configuration d'un serveur DNS dédié pour les noms du réseau distant "
"(ex: 192.168.1.210 pour .local), tout en utilisant le DNS système "
"pour le reste du trafic internet. Le test de connexion vérifie que "
"les domaines configurés s'y résolvent réellement, et un outil de "
"résolution à la demande permet de tester n'importe quel nom précis.",
),
]
GUARANTEES_INTRO = "Ce que l'application garantit, quel que soit le profil configuré :"
GUARANTEES = [
"Le mot de passe du mode Admin n'est jamais stocké en clair : il est "
"haché avec bcrypt (12 tours) avant d'être écrit dans la configuration.",
"La clé privée WireGuard est toujours écrite avec des permissions "
"restrictives (600) — aussi bien dans la configuration locale que dans "
"le fichier installé sous /etc/wireguard.",
"Le test de handshake et le sondage de statut (dernier handshake, "
"RX/TX) n'ouvrent jamais de tunnel et ne modifient aucun fichier : ce "
"sont des lectures ou un simple datagramme UDP jetable.",
"Les commandes système nécessitant les privilèges root sont exécutées "
"une par une, jamais l'application entière : `make setup-sudoers` "
"autorise sans mot de passe uniquement wg-quick, wg, et la suppression "
"d'interface/DNS orpheline — jamais un accès root généralisé.",
"Le sondage périodique de l'état du tunnel (toutes les 3 secondes) "
"n'ouvre jamais de dialogue de mot de passe ou d'UAC : il échoue "
"silencieusement si les privilèges nécessaires ne sont pas déjà "
"accordés.",
"Le test de fuite DNS vérifie que les requêtes sortent bien par le "
"resolver attendu une fois le tunnel monté, plutôt que de supposer la "
"configuration correcte.",
]
TECH_STACK = [
("Interface", "PyQt6"),
("Cryptographie", "cryptography (X25519, ChaCha20-Poly1305, bcrypt)"),
("QR Code", "qrcode/Pillow pour l'export de la config client"),
("Tunnel", "wg-quick / wg (Linux), WireGuard for Windows"),
("Élévation de privilèges", "sudo NOPASSWD ciblé, repli askpass puis pkexec"),
("Empaquetage", "PyInstaller (Linux/Windows) + Inno Setup (installeur Windows)"),
]
# ── Mode d'emploi ────────────────────────────────────────────────────────
HOWTO_FIRST_INTRO = (
"Huit étapes entre une installation nue et un tunnel qui monte. "
"L'ordre compte : le serveur ne peut pas accepter le poste tant qu'il "
"n'a pas reçu sa clé publique."
)
HOWTO_FIRST = [
"Ouvrir <b>⚙️ Administrateur</b> depuis la fenêtre principale.",
"Onglet <b>WireGuard → Tunnel &amp; profils</b> : renseigner l'adresse du "
"serveur, le port UDP et la clé publique du serveur.",
"Onglet <b>WireGuard → Clés</b> : cliquer sur <b>Générer une nouvelle "
"paire de clés</b>, puis transmettre la <b>clé publique</b> affichée à "
"l'administrateur du serveur — sans elle, le serveur refusera la "
"connexion.",
"Retour sur <b>Tunnel &amp; profils</b> : saisir l'adresse IP du client "
"et les IPs autorisées fournies par l'administrateur.",
"Cocher <b>Le serveur utilise une clé pré-partagée</b> uniquement si le "
"serveur en déclare une pour ce poste.",
"Toujours sur <b>Tunnel &amp; profils</b>, renseigner l'<b>Identifiant "
"VPN</b> : le nom du compte tel qu'il figure sur le serveur. Laissé "
"vide, aucun code d'accès n'est demandé à la connexion — le serveur est "
"alors traité comme permissif.",
"Cliquer sur <b>Enregistrer et fermer</b>.",
"Se connecter depuis la fenêtre principale ou le menu du systray, puis "
"saisir le code d'accès à 6 chiffres quand il est demandé.",
]
HOWTO_DAILY = [
(
"Changer de serveur",
"Onglet <b>WireGuard → Tunnel &amp; profils</b> : sélectionner un "
"profil puis <b>✅ Charger</b>.",
),
(
"Reprendre une configuration reçue",
"Même onglet, <b>📂 Importer un .conf</b> : clés, adresses et clé "
"pré-partagée sont repris automatiquement. Les champs issus de "
"l'import se verrouillent, pour ne pas diverger en silence du pair "
"déjà déclaré côté serveur.",
),
(
"Configurer un téléphone",
"<b>📱 QR Code</b>, à scanner depuis l'application WireGuard mobile.",
),
]
HOWTO_TROUBLE = [
(
"Commencer toujours par le diagnostic",
"Onglet <b>Test connexion → Chaîne de connexion</b>, puis <b>🚀 Lancer "
"le test de connexion</b>. Le rapport nomme l'étape qui bloque au "
"lieu de laisser deviner.",
),
(
"Le tunnel monte mais rien ne circule",
"Vérifier que les <b>IPs autorisées</b> ne contiennent pas le réseau "
"local par lequel le serveur est joint : la passerelle serait routée "
"dans le tunnel, qui a justement besoin d'elle.",
),
(
"Plus aucun nom ne se résout après une coupure",
"Onglet <b>Test connexion → Fuite DNS</b>, puis <b>🛠️ Réparer le DNS "
"(tunnel arrêté)</b>.",
),
(
"Une fenêtre d'authentification à chaque connexion",
"Onglet <b>Sécurité</b>, sous-onglet <b>Droits sudo</b> sous Linux ou "
"<b>Élévation</b> sous Windows : il nomme ce qui manque et comment "
"l'accorder.",
),
(
"Le code d'accès est refusé",
"Le code est validé par le serveur, pas par ce poste : un refus vient "
"du serveur et son message est repris tel quel dans le journal.",
),
]
# ── Prérequis ────────────────────────────────────────────────────────────
REQUIREMENTS = [
("🐧 Linux — paquets", [
(
"wireguard-tools <i>(indispensable)</i>",
"Fournit <code>wg</code> et <code>wg-quick</code>, qui montent et "
"démontent le tunnel.<br>"
"<code>sudo apt install wireguard-tools</code> — Debian, Ubuntu"
"<br><code>sudo dnf install wireguard-tools</code> — Fedora",
),
(
"iproute2 <i>(indispensable)</i>",
"<code>ip</code> sert à supprimer une interface restée en place "
"après un arrêt brutal.",
),
(
"coreutils <i>(indispensable)</i>",
"<code>install</code> dépose la configuration dans "
"/etc/wireguard avec le bon propriétaire et les bonnes "
"permissions, en une seule commande privilégiée.",
),
(
"resolvconf ou systemd-resolved <i>(optionnel)</i>",
"Nécessaire pour que la directive DNS du tunnel soit appliquée. "
"En leur absence, le tunnel monte sans toucher au DNS du système "
"plutôt que d'échouer à mi-course.",
),
]),
("🪟 Windows", [
(
"WireGuard for Windows <i>(indispensable)</i>",
"<code>wireguard.exe</code> installe et retire le service du "
"tunnel. L'installeur officiel est embarqué dans le programme "
"d'installation de WGSecure, qui le lance si WireGuard est absent "
"du poste.",
),
(
"Droits administrateur",
"Exigés pour installer ou retirer le service. Le service n'est "
"installé qu'une fois : les démarrages suivants n'ouvrent plus "
"de fenêtre UAC.",
),
]),
("🔌 Ports réseau", [
(
"UDP 51820 — sortant, poste client",
"Port par défaut du serveur WireGuard ; ajustable dans l'onglet "
"Tunnel & profils. C'est le seul flux que l'application émet "
"vers le serveur.",
),
(
"UDP 51820 — entrant, côté serveur",
"Le serveur doit écouter dessus, et le routeur doit y rediriger "
"le trafic venant d'Internet. Sans cette redirection, le "
"handshake n'aboutit jamais et rien ne le signale : WireGuard "
"ignore silencieusement les paquets qu'il ne peut pas "
"authentifier.",
),
(
"Filtrage sur le port source",
"Un pare-feu ne doit pas exiger un port source particulier : un "
"client WireGuard émet depuis un port éphémère aléatoire. Le "
"diagnostic détecte ce cas.",
),
(
"UDP/TCP 53 — DNS",
"Résolution du nom du serveur avant connexion, puis résolutions "
"dans le tunnel une fois monté.",
),
]),
("🔑 Privilèges", [
(
"Linux — root, sudo ou pkexec",
"Demandés commande par commande, au moment du besoin. Une règle "
"sudo évite la fenêtre d'authentification répétée ; le sous-onglet "
"<b>Sécurité → Droits sudo</b> indique lesquelles manquent.",
),
(
"Windows — administrateur",
"Pour l'installation et le retrait du service du tunnel.",
),
]),
("🐍 Depuis les sources", [
(
"Python 3.11 ou supérieur",
"Dépendances : PyQt6, cryptography, qrcode, Pillow, bcrypt. Les "
"binaires distribués les embarquent : rien à installer.",
),
]),
]
# ── Sécurité ─────────────────────────────────────────────────────────────
SECURITY = [
("Authentification", [
(
"Mot de passe administrateur haché avec bcrypt, coût 12",
"Le mot de passe n'est jamais stocké, même chiffré : seule son "
"empreinte l'est, avec un sel propre à chaque enregistrement.",
),
(
"Vérification résistante à l'analyse temporelle",
"<code>bcrypt.checkpw</code> compare l'empreinte sans laisser sa "
"durée trahir le nombre de caractères corrects. Les anciennes "
"empreintes SHA-256, encore acceptées le temps d'une migration, "
"passent par <code>secrets.compare_digest</code> pour la même "
"raison.",
),
(
"Code d'accès validé par le serveur",
"Le code à 6 chiffres part au serveur, seul détenteur du secret "
"et seul point où l'accès est réellement ouvert. L'ancien "
"vérificateur local ne verrouillait que sa propre fenêtre : le "
"secret vivait sur le poste, et le contourner suffisait. Un "
"secret hérité d'une version ≤ 0.8.0 est effacé au démarrage.",
),
]),
("Cryptographie", [
(
"Clés Curve25519 générées par la bibliothèque <code>cryptography</code>",
"Échange X25519, l'algorithme même de WireGuard. L'application ne "
"dépend pas du binaire <code>wg</code> pour produire ses clés.",
),
(
"Aléa fourni par le système d'exploitation",
"Clés pré-partagées et index de session tirés de "
"<code>os.urandom</code>, jamais d'un générateur pseudo-aléatoire "
"ordinaire.",
),
(
"Clé pré-partagée de 32 octets",
"Longueur imposée par WireGuard ; le format est vérifié avant "
"d'être accepté.",
),
]),
("Protection des secrets au repos", [
(
"Configuration du tunnel en 0600",
"Le fichier contenant la clé privée n'est lisible que par son "
"propriétaire, y compris le fichier temporaire employé pour "
"l'installer dans /etc/wireguard — créé directement en 0600, "
"jamais lisible par d'autres, même brièvement.",
),
(
"Clés privées masquées à l'écran",
"Affichées en points, révélées seulement sur demande explicite.",
),
(
"Export .conf également restreint",
"Un fichier exporté reçoit les mêmes permissions que la "
"configuration d'origine.",
),
]),
("Réduction de la surface d'attaque", [
(
"Privilèges demandés au coup par coup",
"L'application ne tourne pas en permanence avec les droits "
"d'administration : chaque commande privilégiée est élevée "
"séparément, par sudo puis pkexec.",
),
(
"Aucun appel passé par un interpréteur de commandes",
"Les commandes système sont invoquées sous forme de liste "
"d'arguments, ce qui exclut l'injection par le contenu d'un champ "
"de configuration.",
),
(
"Sonde de handshake sans état",
"Le test de connexion négocie une poignée de main puis jette "
"immédiatement la session : il ne monte aucun tunnel et ne "
"conserve aucun secret dérivé.",
),
(
"Nettoyage après un arrêt brutal",
"Interface orpheline et configuration DNS résiduelle sont "
"retirées, pour ne pas laisser le système dans un état "
"intermédiaire.",
),
]),
]
def parse_changelog() -> list[dict]:
"""Lit CHANGELOG.md : titres `## [x.y.z] — date`, sous-titres `### ...`
et puces `- ...` (avec leurs lignes de continuation) uniquement — le
format effectivement utilisé par ce fichier, sans dépendance Markdown.
"""
entries: list[dict] = []
version = None
section = None
try:
with open(_CHANGELOG_PATH, encoding="utf-8") as f:
for raw in f:
line = raw.rstrip("\n")
if line.startswith("## "):
version = {"title": line[3:].strip(), "sections": []}
entries.append(version)
section = None
elif line.startswith("### ") and version is not None:
section = {"name": line[4:].strip(), "items": []}
version["sections"].append(section)
elif line.startswith("- ") and section is not None:
section["items"].append(line[2:].strip())
elif (line.strip() and section is not None and section["items"]
and not line.startswith(("#", "---"))):
# Ligne de continuation d'une puce étalée sur plusieurs lignes.
section["items"][-1] += " " + line.strip()
except FileNotFoundError:
pass
return entries