"""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 ⚙️ Administrateur depuis la fenêtre principale.", "Onglet WireGuard → Tunnel & profils : renseigner l'adresse du " "serveur, le port UDP et la clé publique du serveur.", "Onglet WireGuard → Clés : cliquer sur Générer une nouvelle " "paire de clés, puis transmettre la clé publique affichée à " "l'administrateur du serveur — sans elle, le serveur refusera la " "connexion.", "Retour sur Tunnel & profils : saisir l'adresse IP du client " "et les IPs autorisées fournies par l'administrateur.", "Cocher Le serveur utilise une clé pré-partagée uniquement si le " "serveur en déclare une pour ce poste.", "Toujours sur Tunnel & profils, renseigner l'Identifiant " "VPN : 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 Enregistrer et fermer.", "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 WireGuard → Tunnel & profils : sélectionner un " "profil puis ✅ Charger.", ), ( "Reprendre une configuration reçue", "Même onglet, 📂 Importer un .conf : 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", "📱 QR Code, à scanner depuis l'application WireGuard mobile.", ), ] HOWTO_TROUBLE = [ ( "Commencer toujours par le diagnostic", "Onglet Test connexion → Chaîne de connexion, puis 🚀 Lancer " "le test de connexion. Le rapport nomme l'étape qui bloque au " "lieu de laisser deviner.", ), ( "Le tunnel monte mais rien ne circule", "Vérifier que les IPs autorisées 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 Test connexion → Fuite DNS, puis 🛠️ Réparer le DNS " "(tunnel arrêté).", ), ( "Une fenêtre d'authentification à chaque connexion", "Onglet Sécurité, sous-onglet Droits sudo sous Linux ou " "Élévation 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 (indispensable)", "Fournit wg et wg-quick, qui montent et " "démontent le tunnel.
" "sudo apt install wireguard-tools — Debian, Ubuntu" "
sudo dnf install wireguard-tools — Fedora", ), ( "iproute2 (indispensable)", "ip sert à supprimer une interface restée en place " "après un arrêt brutal.", ), ( "coreutils (indispensable)", "install 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 (optionnel)", "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 (indispensable)", "wireguard.exe 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 " "Sécurité → Droits sudo 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", "bcrypt.checkpw 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 secrets.compare_digest 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 cryptography", "Échange X25519, l'algorithme même de WireGuard. L'application ne " "dépend pas du binaire wg 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 " "os.urandom, 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