#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Brute-force GUI + raport pentru detectare mesaje ascunse în 'datecriptate.txt'
- MODIFICAT: Suportă intervale (Range) pentru lungimea cheii și a mesajului.
- MODIFICAT: Temă întunecată (Dark/Blue).
- Implicit: keylen=3-4, chars=40-50, topN=30
Autor: ChatGPT (adaptare cerută de utilizator)
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
import hashlib, os, threading, time, itertools

# ----------------- CULORI & TEMĂ (Dark Blue) -----------------
COLOR_BG = "#0d0d0d"       # Aproape negru
COLOR_FG = "#00ffff"       # Cyan electric
COLOR_ENT_BG = "#1a1a1a"   # Gri foarte închis pt input
COLOR_ENT_FG = "#ffffff"   # Scris alb în input
COLOR_BTN_BG = "#003366"   # Albastru închis
COLOR_BTN_FG = "#ffffff"   # Scris alb pe buton
COLOR_ACCENT = "#00ccff"   # Albastru deschis

# ----------------- utilitare bit/byte -----------------
def bytes_to_bits(b: bytes):
    out = []
    for byte in b:
        for i in range(8):
            out.append((byte >> (7-i)) & 1)
    return out

def bits_to_bytes(bits):
    extra = (-len(bits)) % 8
    if extra:
        bits = bits + [0]*extra
    out = bytearray()
    for i in range(0, len(bits), 8):
        byte = 0
        for j in range(8):
            byte = (byte << 1) | bits[i+j]
        out.append(byte)
    return bytes(out)

def bits_to_str(bits, encoding='utf-8'):
    b = bits_to_bytes(bits)
    try:
        return b.rstrip(b'\x00').decode(encoding)
    except Exception:
        return b.decode(encoding, errors='replace')

# ----------------- keystream & poziții -----------------
def sha256_stream(key: bytes, nbits: int):
    out_bits = []
    counter = 0
    while len(out_bits) < nbits:
        h = hashlib.sha256()
        h.update(key)
        h.update(counter.to_bytes(8,'big'))
        block = h.digest()
        for byte in block:
            for i in range(8):
                out_bits.append((byte >> (7-i)) & 1)
        counter += 1
    return out_bits[:nbits]

def deterministic_sample_positions(key: bytes, n: int, k: int):
    if k > n:
        raise ValueError("k > n")
    idx = list(range(n))
    ctr = 0
    for i in range(n-1, n-k-1, -1):
        h = hashlib.sha256()
        h.update(key); h.update(ctr.to_bytes(8,'big'))
        rnd = int.from_bytes(h.digest()[:8],'big')
        j = rnd % (i+1)
        idx[i], idx[j] = idx[j], idx[i]
        ctr += 1
    return idx[-k:]

def extract_from_stego(stego_bits, key, m_bits):
    n = len(stego_bits)
    if m_bits > n:
        raise ValueError("m_bits > n")
    keyb = key.encode('utf-8')
    pos = deterministic_sample_positions(keyb + b'::pos', n, m_bits)
    payload = [stego_bits[p] for p in pos]
    ks = sha256_stream(keyb + b'::ks::msg', m_bits)
    msg_bits = [p ^ k for p,k in zip(payload, ks)]
    return bits_to_str(msg_bits)

# ----------------- citire fișier -----------------
def read_bitfile(path):
    bits = []
    with open(path,'r', encoding='utf-8', errors='ignore') as f:
        for line in f:
            for ch in line.strip():
                if ch == '0' or ch == '1':
                    bits.append(1 if ch=='1' else 0)
    return bits

# ----------------- heuristici scorare -----------------
COMMON_WORDS = [' the ',' and ',' of ',' to ',' in ',' is ',' that ',' it ',' for ',' on ',
                ' şi ',' si ',' este ',' ca ',' în ',' de ',' la ',' cu ',' pentru ', ' salut ', ' am ', ' mesaj ']

def printable_ratio(s: str):
    if not s: return 0.0
    ok = 0
    for ch in s:
        code = ord(ch)
        if 32 <= code <= 126 or ch in '\n\r\t':
            ok += 1
    return ok / len(s)

def common_word_score(s: str):
    low = s.lower()
    score = 0
    for w in COMMON_WORDS:
        if w.strip() and w in low:
            score += 1
    return score

def score_candidate(text: str):
    pr = printable_ratio(text)
    cw = common_word_score(text)
    length = len(text)
    score = pr * 0.7 + min(1.0, cw/3.0) * 0.25
    if length >= 5: score += 0.05
    return score

# ----------------- GUI + worker -----------------
class BruteGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Brute-force Avansat — Interval Chei & Caractere")
        self.geometry("1000x750")
        self.configure(bg=COLOR_BG)

        # Configurare stiluri pentru Dark Mode
        style = ttk.Style()
        style.theme_use('clam')
        style.configure("Treeview", 
                        background="#202020", 
                        foreground="white", 
                        fieldbackground="#202020", 
                        rowheight=25)
        style.map('Treeview', background=[('selected', COLOR_BTN_BG)])
        style.configure("Treeview.Heading", background="#333333", foreground="white", font=('Arial', 10, 'bold'))
        style.configure("Horizontal.TProgressbar", background=COLOR_ACCENT, troughcolor="#333333")

        # --- Zona controale ---
        frm = tk.Frame(self, bg=COLOR_BG)
        frm.pack(fill='x', padx=15, pady=15)

        # Helper pentru label-uri colorate
        def lbl(r, c, txt):
            l = tk.Label(frm, text=txt, bg=COLOR_BG, fg=COLOR_FG, font=('Arial', 10))
            l.grid(row=r, column=c, sticky='w', padx=5, pady=5)
            return l

        # Helper pentru entry-uri colorate
        def ent(r, c, val, width=10):
            e = tk.Entry(frm, width=width, bg=COLOR_ENT_BG, fg=COLOR_ENT_FG, insertbackground='white')
            e.grid(row=r, column=c, sticky='w', padx=5, pady=5)
            e.insert(0, str(val))
            return e

        # Rând 0: Fișier
        lbl(0, 0, "Fișier stego:")
        self.ent_file = tk.Entry(frm, width=70, bg=COLOR_ENT_BG, fg=COLOR_ENT_FG, insertbackground='white')
        self.ent_file.grid(row=0, column=1, columnspan=4, sticky='w', padx=5)
        self.ent_file.insert(0, "datecriptate.txt")
        tk.Button(frm, text="Browse", bg=COLOR_BTN_BG, fg=COLOR_BTN_FG, command=self.browse).grid(row=0, column=5, padx=5)

        # Rând 1: Interval Lungime Cheie
        lbl(1, 0, "Lungime Cheie (biți):")
        lbl(1, 1, "De la:")
        self.ent_key_min = ent(1, 2, "1")
        lbl(1, 3, "La:")
        self.ent_key_max = ent(1, 4, "4")

        # Rând 2: Interval Caractere Mesaj
        lbl(2, 0, "Lungime Mesaj (char):")
        lbl(2, 1, "De la:")
        self.ent_char_min = ent(2, 2, "40")
        lbl(2, 3, "La:")
        self.ent_char_max = ent(2, 4, "60")

        # Rând 3: Top results & Butoane
        lbl(3, 0, "Top rezultate afișate:")
        self.ent_topn = ent(3, 1, "30", width=8)

        tk.Button(frm, text="▶ PORNEȘTE BRUTE-FORCE", bg=COLOR_ACCENT, fg='black', font=('Arial', 10, 'bold'), command=self.start).grid(row=4, column=1, columnspan=2, pady=15, sticky='w')
        tk.Button(frm, text="X ANULEAZĂ", bg='#990000', fg='white', font=('Arial', 10, 'bold'), command=self.cancel).grid(row=4, column=3, pady=15, sticky='w')

        # Bara progres
        self.progress = ttk.Progressbar(self, orient='horizontal', length=960, mode='determinate')
        self.progress.pack(padx=15, pady=5)

        # Tabel rezultate
        cols = ('key','chars','score','pr_ratio','common_words','message_preview')
        self.tree = ttk.Treeview(self, columns=cols, show='headings', height=15)
        for c in cols:
            self.tree.heading(c, text=c.upper())
            width = 100 if c != 'message_preview' else 450
            self.tree.column(c, width=width, anchor='w')
        self.tree.pack(padx=15, pady=5, fill='both', expand=True)

        # Log
        lbl_log = tk.Label(self, text="Jurnal de execuție:", bg=COLOR_BG, fg="#aaaaaa")
        lbl_log.pack(anchor='w', padx=15)
        self.log = scrolledtext.ScrolledText(self, width=116, height=8, bg="#111111", fg="#00ff00", insertbackground='white')
        self.log.pack(padx=15, pady=(0,15), fill='x')

        self._worker = None
        self._stop_flag = threading.Event()
        self.candidates_final = []

    def browse(self):
        p = filedialog.askopenfilename(title="Alege fișier stego", filetypes=[("Text files","*.txt"),("All","*.*")])
        if p: self.ent_file.delete(0,'end'); self.ent_file.insert(0,p)

    def logmsg(self, s):
        self.log.insert('end', f"> {s}\n"); self.log.see('end')

    def start(self):
        if self._worker and self._worker.is_alive():
            messagebox.showwarning("Atentie", "Procesul rulează deja.")
            return
        
        path = self.ent_file.get().strip() or "datecriptate.txt"
        if not os.path.exists(path):
            messagebox.showerror("Eroare", f"Fișier inexistent: {path}"); return

        try:
            k_min = int(self.ent_key_min.get().strip())
            k_max = int(self.ent_key_max.get().strip())
            c_min = int(self.ent_char_min.get().strip())
            c_max = int(self.ent_char_max.get().strip())
            topn = int(self.ent_topn.get().strip())
        except ValueError:
            messagebox.showerror("Eroare", "Te rog introdu doar numere întregi."); return

        if k_min > k_max: k_min, k_max = k_max, k_min
        if c_min > c_max: c_min, c_max = c_max, c_min
        if c_min < 1: c_min = 1

        bits = read_bitfile(path)
        if not bits:
            messagebox.showerror("Eroare", "Fișierul nu conține biți validi."); return

        # Reset UI
        for i in self.tree.get_children(): self.tree.delete(i)
        self.progress['value'] = 0
        self.candidates_final = []
        self._stop_flag.clear()

        # Calcul estimativ pași totali
        total_steps = 0
        char_range_len = (c_max - c_min + 1)
        for kl in range(k_min, k_max + 1):
            total_steps += (2 ** kl) * char_range_len
        
        self.progress['maximum'] = total_steps
        
        self.logmsg(f"START: Chei {k_min}-{k_max} biți | Mesaje {c_min}-{c_max} chars")
        self.logmsg(f"Total combinații de verificat: {total_steps}")

        self._worker = threading.Thread(
            target=self._worker_func, 
            args=(bits, k_min, k_max, c_min, c_max, topn, path), 
            daemon=True
        )
        self._worker.start()

    def cancel(self):
        if self._worker and self._worker.is_alive():
            self._stop_flag.set()
            self.logmsg("!!! Stop solicitat. Se oprește...")
        else:
            self.logmsg("Nu rulează nimic momentan.")

    def _worker_func(self, bits, k_min, k_max, c_min, c_max, topn, path):
        candidates = []
        checked = 0
        start = time.time()
        
        # Iterăm prin lungimile de cheie
        for keylen in range(k_min, k_max + 1):
            if self._stop_flag.is_set(): break
            
            # Generăm toate cheile de lungimea keylen
            for kbits in itertools.product('01', repeat=keylen):
                if self._stop_flag.is_set(): break
                key = ''.join(kbits)
                
                # Iterăm prin lungimile de caractere
                for chars in range(c_min, c_max + 1):
                    if self._stop_flag.is_set(): break
                    
                    m_bits = chars * 8
                    if m_bits > len(bits): break # Mesaj prea lung pt fișier

                    try:
                        msg = extract_from_stego(bits, key, m_bits)
                    except Exception:
                        msg = ""
                    
                    # Scorare
                    pr = printable_ratio(msg)
                    cw = common_word_score(msg)
                    sc = score_candidate(msg)
                    
                    checked += 1
                    
                    # Update Progress (mai rar, pt performanță)
                    if checked % 50 == 0:
                        self.progress['value'] = checked

                    # Filtru de calitate (salvăm doar ce pare text real)
                    if pr >= 0.85 or sc > 0.55 or cw >= 1:
                        candidates.append((sc, key, chars, pr, cw, msg))
                        # Păstrăm top N * 5 în memorie
                        candidates.sort(key=lambda x: x[0], reverse=True)
                        if len(candidates) > topn * 10:
                            candidates = candidates[:topn * 10]

                # Update tabel vizual periodic
                if checked % 500 == 0:
                    self._update_table(candidates, topn)

        # Finalizare
        self._update_table(candidates, topn)
        elapsed = time.time() - start
        self.logmsg(f"GATA. Timp: {elapsed:.2f}s, Verificări: {checked}")
        
        self.candidates_final = candidates[:]
        self._save_report(path, checked, elapsed)

    def _update_table(self, candidates, topn):
        def ui_update():
            for i in self.tree.get_children():
                self.tree.delete(i)
            
            seen = set()
            shown = 0
            # Afișăm doar topN
            for sc, key, chars, pr, cw, msg in candidates:
                if shown >= topn: break
                
                # Evităm duplicate vizuale (cheie, lungime)
                unique_id = (key, chars)
                if unique_id in seen: continue
                seen.add(unique_id)
                
                preview = msg.replace('\n','\\n')[:100]
                self.tree.insert('', 'end', values=(key, chars, f"{sc:.4f}", f"{pr:.3f}", str(cw), preview))
                shown += 1
        self.after(1, ui_update)

    def _save_report(self, path, attempts, elapsed):
        report_path = os.path.join(os.getcwd(), "bruteforce_report.txt")
        try:
            with open(report_path, 'w', encoding='utf-8') as f:
                f.write("=== RAPORT BRUTE-FORCE STEGO ===\n")
                f.write(f"Data: {time.ctime()}\n")
                f.write(f"Fișier: {path}\n")
                f.write(f"Statistici: {attempts} încercări în {elapsed:.2f} secunde\n")
                f.write("="*60 + "\n\n")
                
                if not self.candidates_final:
                    f.write("REZULTAT: Niciun candidat viabil găsit.\n")
                else:
                    f.write(f"TOP CANDIDAȚI GĂSIȚI (descrescător după scor):\n\n")
                    for sc, key, chars, pr, cw, msg in self.candidates_final:
                        f.write(f"[SCOR: {sc:.4f}] KEY='{key}' | LEN={chars} chars\n")
                        f.write(f"Printable: {pr:.2f} | Cuvinte comune: {cw}\n")
                        f.write(f"Mesaj:\n{msg}\n")
                        f.write("-" * 60 + "\n")
            
            self.logmsg(f"Raport salvat în: {report_path}")
            messagebox.showinfo("Succes", f"Raport salvat cu succes!\n{report_path}")
        except Exception as e:
            self.logmsg(f"Eroare scriere raport: {e}")

if __name__ == '__main__':
    app = BruteGUI()
    app.mainloop()
