#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SecureWipe v5.3 - Effacement sécurisé renforcé.
- Fenêtre 480x560 (bouton EFFACER toujours visible)
- DnD via tkinterdnd2 (auto-install)
- Statut DnD fusionné dans la barre de statut du bas
"""
import os
import re
import sys
import stat
import shutil
import threading
import random
import string
import subprocess
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
# =========================================================
# AUTO-INSTALL tkinterdnd2
# =========================================================
def _try_import_dnd():
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
return True, None
except Exception as e:
return False, str(e)
def _auto_install_dnd():
print("[DND] tkinterdnd2 absent → installation auto…")
cmds = [
[sys.executable, "-m", "pip", "install", "--user", "tkinterdnd2"],
[sys.executable, "-m", "pip", "install", "--user", "--break-system-packages", "tkinterdnd2"],
]
for cmd in cmds:
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if r.returncode == 0:
print("[DND] Installation réussie.")
return True
except Exception as e:
print(f"[DND] Erreur : {e}")
return False
DND_AVAILABLE, DND_ERROR = _try_import_dnd()
if not DND_AVAILABLE:
if _auto_install_dnd():
DND_AVAILABLE, DND_ERROR = _try_import_dnd()
if DND_AVAILABLE:
from tkinterdnd2 import DND_FILES, TkinterDnD
# =========================================================
# MOTEUR
# =========================================================
CHUNK_SIZE = 1024 * 1024
DEFAULT_PASSES = 5
RENAME_ROUNDS = 3
PATTERNS = [
lambda n: b"\x00" * n, lambda n: b"\xFF" * n,
lambda n: b"\xAA" * n, lambda n: b"\x55" * n,
lambda n: b"\x92" * n, lambda n: b"\x49" * n,
lambda n: b"\x24" * n, lambda n: os.urandom(n),
]
def _sync_dir(path: Path):
try:
fd = os.open(str(path), os.O_RDONLY)
try: os.fsync(fd)
finally: os.close(fd)
except OSError: pass
def _random_name(length=None):
if length is None: length = random.randint(12, 24)
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
def _overwrite_file(path: Path, passes: int) -> bool:
try: size = path.stat().st_size
except OSError: return False
if size == 0: return True
try:
with open(path, "r+b", buffering=0) as f:
for p in range(passes):
f.seek(0); written = 0
while written < size:
block = min(CHUNK_SIZE, size - written)
f.write(PATTERNS[p % len(PATTERNS)](block))
written += block
f.flush(); os.fsync(f.fileno())
f.seek(0); f.truncate(0); f.flush(); os.fsync(f.fileno())
f.write(os.urandom(256)); f.flush(); os.fsync(f.fileno())
f.seek(0); f.truncate(0); f.flush(); os.fsync(f.fileno())
return True
except (OSError, PermissionError): return False
def _force_remove(path: Path):
try: os.chmod(path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
except OSError: pass
try:
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path, ignore_errors=True)
else:
path.unlink(missing_ok=True)
except OSError: pass
def _erase_single_file(fp: Path, passes: int) -> bool:
if not _overwrite_file(fp, passes): return False
current = fp
for _ in range(RENAME_ROUNDS):
try:
new_name = current.parent / _random_name()
current.rename(new_name)
_sync_dir(current.parent)
current = new_name
except OSError: break
_force_remove(current)
_sync_dir(fp.parent)
return True
def secure_erase(target: Path, passes=DEFAULT_PASSES, log_cb=None):
target = Path(target)
if not target.exists():
if log_cb: log_cb(f"⚠ Introuvable : {target}")
return 0, 0
erased = failed = 0
if target.is_file() or target.is_symlink():
if log_cb: log_cb(f"→ {target.name} ({passes} passes)")
if _erase_single_file(target, passes):
erased += 1
if log_cb: log_cb(f" ✔ Effacé : {target.name}")
else:
failed += 1
if log_cb: log_cb(f" ✘ Échec : {target.name}")
return erased, failed
for root, dirs, files in os.walk(target, topdown=False):
root_path = Path(root)
for fname in files:
fp = root_path / fname
if log_cb: log_cb(f"→ {fp.relative_to(target)} ({passes} passes)")
if _erase_single_file(fp, passes): erased += 1
else:
failed += 1
if log_cb: log_cb(f" ✘ Échec : {fp.relative_to(target)}")
for dname in dirs:
_force_remove(root_path / dname)
_force_remove(target)
_sync_dir(target.parent)
if log_cb: log_cb(f"✔ Dossier supprimé : {target.name}")
return erased, failed
def _parse_clipboard_paths(raw: str):
if not raw: return []
paths = []
for m in re.finditer(r"file://([^\s\"'<>]+)", raw):
p = m.group(1)
try:
from urllib.parse import unquote
p = unquote(p)
except Exception: pass
paths.append(p)
if paths: return paths
for t in re.findall(r'"[^"]+"|\'[^\']+\'|\S+', raw):
t = t.strip().strip('"').strip("'")
if t: paths.append(t)
return paths
# =========================================================
# UI
# =========================================================
class SecureWipeApp:
def __init__(self, root):
self.root = root
self.root.title("SecureWipe v5.3")
self.root.geometry("480x560")
self.root.minsize(440, 540)
self.root.resizable(False, False)
self.root.configure(bg="#1e1e2e")
self.targets = []
self.running = False
self._build_ui()
self._register_dnd()
self._check_dnd_engine()
def _build_ui(self):
BG = "#1e1e2e"; CARD = "#2a2a3e"; DROP_BG = "#32324a"; DROP_HI = "#554488"
ACCENT = "#ff5555"; ACCENT_H = "#ff7070"
TEXT = "#f8f8f2"; SUBTEXT = "#a0a0b8"; GREEN = "#50fa7b"
style = ttk.Style(); style.theme_use("clam")
style.configure("Accent.TButton", background=ACCENT, foreground="white",
font=("Segoe UI", 9, "bold"), borderwidth=0, padding=6)
style.map("Accent.TButton", background=[("active", ACCENT_H)])
style.configure("Ghost.TButton", background=CARD, foreground=TEXT,
font=("Segoe UI", 8), borderwidth=0, padding=4)
style.map("Ghost.TButton", background=[("active", "#3a3a52")])
# Titre
tk.Label(self.root, text="🗑 SecureWipe v5.3",
bg=BG, fg=TEXT, font=("Segoe UI", 13, "bold")).pack(pady=(8, 4))
# Zone de drop
self.drop_zone = tk.Label(
self.root,
text="⬇ GLISSEZ-DÉPOSEZ ICI ⬇\n"
"fichiers ou dossiers\n"
"(ou Ctrl+V pour coller un chemin)",
bg=DROP_BG, fg=SUBTEXT,
font=("Segoe UI", 9, "bold"),
relief="solid", bd=2,
height=3, justify="center"
)
self.drop_zone.pack(padx=10, pady=(0, 4), fill="x")
self.drop_zone.bind("<Control-v>", self._on_paste)
self.drop_zone.bind("<Control-V>", self._on_paste)
# Champ chemin
path_frame = tk.Frame(self.root, bg=BG)
path_frame.pack(padx=10, pady=(0, 4), fill="x")
tk.Label(path_frame, text="Chemin :", bg=BG, fg=SUBTEXT,
font=("Segoe UI", 8)).pack(side="left")
self.path_var = tk.StringVar()
entry = tk.Entry(path_frame, textvariable=self.path_var,
bg=CARD, fg=TEXT, insertbackground=TEXT,
relief="flat", font=("Consolas", 8))
entry.pack(side="left", fill="x", expand=True, padx=(4, 4))
entry.bind("<Return>", lambda e: self._add_manual_path())
ttk.Button(path_frame, text="+", style="Ghost.TButton", width=3,
command=self._add_manual_path).pack(side="right")
# Boutons
btn_frame = tk.Frame(self.root, bg=BG)
btn_frame.pack(padx=10, pady=(2, 4), fill="x")
ttk.Button(btn_frame, text="📂 Fichiers", style="Ghost.TButton",
command=lambda: self._browse(True)).pack(side="left")
ttk.Button(btn_frame, text="📁 Dossier", style="Ghost.TButton",
command=lambda: self._browse(False)).pack(side="left", padx=4)
ttk.Button(btn_frame, text="🧹 Vider", style="Ghost.TButton",
command=self._clear_list).pack(side="left")
# Liste
list_frame = tk.Frame(self.root, bg=CARD, bd=0)
list_frame.pack(padx=10, pady=(2, 4), fill="both", expand=True)
self.listbox = tk.Listbox(list_frame, bg=CARD, fg=TEXT,
selectbackground="#3a3a52", selectforeground=TEXT,
font=("Consolas", 8), borderwidth=0,
highlightthickness=0, activestyle="none", height=4)
self.listbox.pack(side="left", fill="both", expand=True, padx=(6, 0), pady=6)
scroll = tk.Scrollbar(list_frame, command=self.listbox.yview)
scroll.pack(side="right", fill="y", pady=6)
self.listbox.config(yscrollcommand=scroll.set)
# Passes
passes_frame = tk.Frame(self.root, bg=BG)
passes_frame.pack(padx=10, pady=(0, 2), fill="x")
tk.Label(passes_frame, text="Passes :", bg=BG, fg=SUBTEXT,
font=("Segoe UI", 8)).pack(side="left")
self.passes_var = tk.IntVar(value=DEFAULT_PASSES)
tk.Spinbox(passes_frame, from_=1, to=35, textvariable=self.passes_var,
width=3, bg=CARD, fg=TEXT, insertbackground=TEXT,
buttonbackground=CARD, relief="flat",
font=("Segoe UI", 8)).pack(side="left", padx=4)
tk.Label(passes_frame, text="5=défaut 7=DoD 35=Gutmann",
bg=BG, fg=SUBTEXT, font=("Segoe UI", 7)).pack(side="left")
# Progression
self.progress = ttk.Progressbar(self.root, mode="indeterminate")
self.progress.pack(padx=10, pady=(2, 2), fill="x")
# Statut (fusionne info DnD + état)
self.status = tk.Label(self.root, text="Prêt.", bg=BG, fg=SUBTEXT,
font=("Segoe UI", 8), anchor="w")
self.status.pack(padx=10, fill="x")
# Journal
self.log = tk.Text(self.root, height=3, bg=CARD, fg=TEXT,
font=("Consolas", 7), borderwidth=0,
highlightthickness=0, state="disabled")
self.log.pack(padx=10, pady=(2, 4), fill="both")
# Bouton principal
self.erase_btn = ttk.Button(self.root, text="🔥 EFFACER (5 passes)",
style="Accent.TButton", command=self._start_erase)
self.erase_btn.pack(padx=10, pady=(0, 8), fill="x")
self.root.bind("<Control-v>", self._on_paste)
self.root.bind("<Control-V>", self._on_paste)
# ---------- DnD ----------
def _register_dnd(self):
if not DND_AVAILABLE:
print(f"[DND] Indisponible : {DND_ERROR}")
return
try:
self.root.drop_target_register(DND_FILES)
self.root.dnd_bind('<<Drop>>', self._on_drop)
self.root.dnd_bind('<<DropEnter>>', self._on_drag_enter)
self.root.dnd_bind('<<DropLeave>>', self._on_drag_leave)
self.drop_zone.drop_target_register(DND_FILES)
self.drop_zone.dnd_bind('<<Drop>>', self._on_drop)
self.drop_zone.dnd_bind('<<DropEnter>>', self._on_drag_enter)
self.drop_zone.dnd_bind('<<DropLeave>>', self._on_drag_leave)
self.listbox.drop_target_register(DND_FILES)
self.listbox.dnd_bind('<<Drop>>', self._on_drop)
print("[DND] Enregistré (root + drop_zone + listbox)")
except Exception as e:
print(f"[DND] Échec enregistrement : {e}")
def _check_dnd_engine(self):
"""Affiche l'état DnD dans la barre de statut (pas de ligne en plus)."""
if not DND_AVAILABLE:
self.status.config(
text="⚠ DnD indisponible — utilisez Ctrl+V ou les boutons",
fg="#ffaa55")
return
try:
v = self.root.tk.call('package', 'require', 'tkdnd')
print(f"[DND] tkdnd chargé : v{v}")
self.status.config(text=f"✅ DnD actif (tkdnd v{v}) — prêt.",
fg="#50fa7b")
except Exception as e:
print(f"[DND] tkdnd NON chargé : {e}")
self.status.config(
text=f"⚠ tkdnd binaire non chargé — Ctrl+V ou boutons",
fg="#ff5555")
def _on_drag_enter(self, event):
self.drop_zone.config(bg="#554488", fg="#ffffff")
def _on_drag_leave(self, event):
self.drop_zone.config(bg="#32324a", fg="#a0a0b8")
def _on_drop(self, event):
print(f"[DROP] reçu : {event.data!r}")
try:
paths = self.root.tk.splitlist(event.data)
except Exception as e:
print(f"[DROP] splitlist échec : {e}")
paths = [event.data]
self._add_paths(list(paths))
self.drop_zone.config(bg="#32324a", fg="#a0a0b8")
# ---------- Collage ----------
def _on_paste(self, event=None):
try: raw = self.root.clipboard_get()
except tk.TclError:
self._set_status("Presse-papiers vide.", "#ffaa55")
return "break"
paths = _parse_clipboard_paths(raw)
if not paths:
self._set_status("Aucun chemin détecté.", "#ffaa55")
return "break"
self._add_paths(paths)
return "break"
# ---------- Helpers ----------
def _log(self, msg):
def _do():
self.log.config(state="normal")
self.log.insert("end", msg + "\n")
self.log.see("end")
self.log.config(state="disabled")
self.root.after(0, _do)
def _set_status(self, msg, color="#a0a0b8"):
self.root.after(0, lambda: self.status.config(text=msg, fg=color))
def _add_manual_path(self):
p = self.path_var.get().strip()
if p: self._add_paths([p]); self.path_var.set("")
def _add_paths(self, paths):
added = skipped = 0
for p in paths:
try: p = Path(p.strip())
except Exception: skipped += 1; continue
if not p.exists(): skipped += 1; continue
if p in self.targets: continue
self.targets.append(p)
kind = "📁" if p.is_dir() else "📄"
self.listbox.insert("end", f" {kind} {p.name}")
added += 1
if added: self._set_status(f"{len(self.targets)} élément(s).", "#50fa7b")
elif skipped: self._set_status(f"{skipped} ignoré(s).", "#ffaa55")
def _browse(self, is_file):
if is_file:
self._add_paths(filedialog.askopenfilenames(title="Choisir des fichiers"))
else:
folder = filedialog.askdirectory(title="Choisir un dossier")
if folder: self._add_paths([folder])
def _clear_list(self):
self.targets.clear(); self.listbox.delete(0, "end")
self._set_status("Liste vidée.")
def _start_erase(self):
if self.running: return
if not self.targets:
messagebox.showwarning("Rien à effacer", "Ajoutez au moins un élément.")
return
self.running = True
self.erase_btn.config(state="disabled")
self.progress.start(12)
self._set_status("Effacement en cours…", "#ff5555")
self._log("═" * 40)
self._log(f"Démarrage — {int(self.passes_var.get())} passes")
threading.Thread(target=self._worker, daemon=True).start()
def _worker(self):
passes = max(1, int(self.passes_var.get()))
total_erased = total_failed = 0
for target in list(self.targets):
e, f = secure_erase(target, passes=passes, log_cb=self._log)
total_erased += e; total_failed += f
def _done():
self.progress.stop(); self.running = False
self.erase_btn.config(state="normal")
self.targets.clear(); self.listbox.delete(0, "end")
self._log("═" * 40)
self._log(f"Terminé. OK:{total_erased} | Échecs:{total_failed}")
if total_failed == 0:
self._set_status(f"✔ {total_erased} élément(s) effacé(s).", "#50fa7b")
else:
self._set_status(f"⚠ {total_failed} échec(s).", "#ffaa55")
messagebox.showinfo("Terminé",
f"Effacés : {total_erased}\nÉchecs : {total_failed}")
self.root.after(0, _done)
def main():
if DND_AVAILABLE:
root = TkinterDnD.Tk()
print("[DND] TkinterDnD.Tk() utilisé")
else:
root = tk.Tk()
print("[DND] tk.Tk() simple (DnD désactivé)")
SecureWipeApp(root)
root.mainloop()
if __name__ == "__main__":
main()4 views