#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Brute-force STEGO - v3.0
1. Suportă afișare LIVE a cheii curente.
2. Mod nou: "Interferență / Pattern" (pentru detectare structuri matematice, nu doar text).
3. Temă Hacker (Dark/Blue).

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 = "#050505"       # Aproape negru total
COLOR_PANEL = "#111111"    # Gri foarte închis pentru panouri
COLOR_FG = "#00ffff"       # Cyan electric
COLOR_TEXT_DIM = "#aaaaaa" # Text secundar
COLOR_ACCENT = "#00ccff"   # Albastru deschis
COLOR_Live = "#ff3333"     # Roșu pentru indicatorul LIVE

# ----------------- 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:
        # Încercăm decodare, caracterele invalide devin '?'
        return b.decode(encoding, errors='replace')
    except Exception:
        return str(b)

# ----------------- EXTRACTION ENGINE -----------------
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: return list(range(n))[:k] # Fallback
    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)
    keyb = key.encode('utf-8')
    # 1. Determinăm pozițiile
    pos = deterministic_sample_positions(keyb + b'::pos', n, m_bits)
    payload = [stego_bits[p] for p in pos]
    # 2. Generăm keystream pentru XOR
    ks = sha256_stream(keyb + b'::ks::msg', m_bits)
    # 3. Decriptăm
    msg_bits = [p ^ k for p,k in zip(payload, ks)]
    return msg_bits

# ----------------- HEURISTICI & ANALIZĂ -----------------

# 1. Mod TEXT (Original)
COMMON_WORDS = ['the','and','to','in','is','that','for','on','si','de','la','cu','care','mai','sa','te','eu']
def score_text_candidate(bits):
    text = bits_to_str(bits)
    if not text: return 0, text
    
    # Ratia de caractere printabile
    printable = 0
    for ch in text:
        if 32 <= ord(ch) <= 126 or ch in '\n\r\t ':
            printable += 1
    pr_ratio = printable / len(text)
    
    # Scor cuvinte comune
    low = text.lower()
    cw_score = 0
    for w in COMMON_WORDS:
        if f" {w} " in f" {low} ": # căutare cuvânt întreg
            cw_score += 1
            
    final_score = pr_ratio * 0.6 + min(1.0, cw_score/2.0) * 0.4
    if len(text) > 6: final_score += 0.05
    return final_score, text

# 2. Mod INTERFERENȚĂ (Nou - caută alternanță 010101 sau repetiții)
def score_interference_pattern(bits):
    """
    Caută regularitate matematică.
    Interferența maximă (franjuri) este alternanța perfectă: 010101...
    Sau blocuri solide: 000000... 111111...
    """
    if not bits: return 0, ""
    
    n = len(bits)
    if n < 2: return 0, ""
    
    transitions = 0
    for i in range(n-1):
        if bits[i] != bits[i+1]:
            transitions += 1
    
    # Rata de tranziție (0.0 = constant, 1.0 = alternanță perfectă, 0.5 = zgomot random)
    rate = transitions / (n - 1)
    
    # Ne interesează extremele: foarte ordonat (aproape de 0 sau aproape de 1)
    # Zgomotul aleator tinde spre 0.5. Orice deviație mare de la 0.5 e suspectă.
    deviation = abs(rate - 0.5) * 2  # Scalăm 0..1 (unde 0 e random, 1 e pattern puternic)
    
    # Reprezentare vizuală a biților pentru GUI
    bit_preview = "".join(['|' if b==1 else '.' for b in bits[:100]])
    
    return deviation, f"[Rata: {rate:.2f}] {bit_preview}"

# ----------------- GUI -----------------
class BruteGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Stego Hunter - Live Analysis")
        self.geometry("1100x800")
        self.configure(bg=COLOR_BG)
        
        # Variabile Thread-Safe pentru UI Updates
        self.var_live_key = tk.StringVar(value="...")
        self.var_live_score = tk.StringVar(value="...")
        self.search_mode = tk.StringVar(value="text") # 'text' sau 'pattern'

        self._setup_ui()
        
        self._worker = None
        self._stop_flag = threading.Event()
        self.candidates_buffer = [] # Buffer mic pentru transfer thread -> UI
        self.final_candidates = []

        # Pornim ceasul intern pentru update UI
        self._poll_ui_updates()

    def _setup_ui(self):
        # Stiluri
        style = ttk.Style()
        style.theme_use('clam')
        style.configure("Treeview", background="#222", foreground="white", fieldbackground="#222", rowheight=22, borderwidth=0)
        style.map('Treeview', background=[('selected', '#004488')])
        style.configure("Horizontal.TProgressbar", background=COLOR_ACCENT, troughcolor="#222")
        
        # --- ZONA DE CONTROL ---
        frm_ctrl = tk.Frame(self, bg=COLOR_PANEL, bd=1, relief='solid')
        frm_ctrl.pack(fill='x', padx=10, pady=10)

        # Input Fisier
        tk.Label(frm_ctrl, text="FIȘIER:", bg=COLOR_PANEL, fg=COLOR_FG).grid(row=0, column=0, sticky='e', padx=5)
        self.ent_file = tk.Entry(frm_ctrl, bg="#000", fg="white", insertbackground='white', width=60)
        self.ent_file.insert(0, "datecriptate.txt")
        self.ent_file.grid(row=0, column=1, columnspan=2, sticky='w')
        tk.Button(frm_ctrl, text="Browse", bg="#333", fg="white", command=self.browse).grid(row=0, column=3)

        # Input Parametri
        tk.Label(frm_ctrl, text="CHEIE (biți) min-max:", bg=COLOR_PANEL, fg="white").grid(row=1, column=0, sticky='e', padx=5, pady=5)
        self.ent_kmin = tk.Entry(frm_ctrl, width=5, bg="#000", fg="white"); self.ent_kmin.insert(0,"1")
        self.ent_kmin.grid(row=1, column=1, sticky='w')
        self.ent_kmax = tk.Entry(frm_ctrl, width=5, bg="#000", fg="white"); self.ent_kmax.insert(0,"4")
        self.ent_kmax.grid(row=1, column=1, padx=50, sticky='w')

        tk.Label(frm_ctrl, text="MESAJ (len) min-max:", bg=COLOR_PANEL, fg="white").grid(row=1, column=2, sticky='e', padx=5)
        self.ent_cmin = tk.Entry(frm_ctrl, width=5, bg="#000", fg="white"); self.ent_cmin.insert(0,"40")
        self.ent_cmin.grid(row=1, column=3, sticky='w')
        self.ent_cmax = tk.Entry(frm_ctrl, width=5, bg="#000", fg="white"); self.ent_cmax.insert(0,"60")
        self.ent_cmax.grid(row=1, column=3, padx=50, sticky='w')

        # --- SELECȚIE MOD ---
        frm_mode = tk.Frame(self, bg=COLOR_PANEL)
        frm_mode.pack(fill='x', padx=10, pady=0)
        tk.Label(frm_mode, text="MOD DE CĂUTARE:", bg=COLOR_PANEL, fg=COLOR_ACCENT, font=('Arial', 11, 'bold')).pack(side='left', padx=10)
        
        rb1 = tk.Radiobutton(frm_mode, text="Text (Limbaj Natural)", variable=self.search_mode, value="text", 
                             bg=COLOR_PANEL, fg="white", selectcolor="#333", activebackground=COLOR_PANEL, activeforeground=COLOR_ACCENT)
        rb1.pack(side='left', padx=10)
        
        rb2 = tk.Radiobutton(frm_mode, text="Interferență (Pattern/Alternanță Biți)", variable=self.search_mode, value="pattern", 
                             bg=COLOR_PANEL, fg="white", selectcolor="#333", activebackground=COLOR_PANEL, activeforeground=COLOR_ACCENT)
        rb2.pack(side='left', padx=10)

        # Butoane Start
        tk.Button(frm_mode, text="START SCANARE", bg="#006600", fg="white", font=('Arial', 10, 'bold'), width=20, command=self.start).pack(side='right', padx=10, pady=10)
        tk.Button(frm_mode, text="STOP", bg="#660000", fg="white", font=('Arial', 10, 'bold'), width=10, command=self.cancel).pack(side='right', padx=0, pady=10)

        # --- LIVE MONITOR ---
        frm_live = tk.Frame(self, bg="#000", bd=2, relief="sunken")
        frm_live.pack(fill='x', padx=10, pady=10)
        
        tk.Label(frm_live, text="SCANARE LIVE:", bg="#000", fg=COLOR_Live, font=('Consolas', 10, 'bold')).pack(side='left', padx=10)
        tk.Label(frm_live, textvariable=self.var_live_key, bg="#000", fg=COLOR_ACCENT, font=('Consolas', 10)).pack(side='left', padx=10)
        tk.Label(frm_live, textvariable=self.var_live_score, bg="#000", fg="#ffff00", font=('Consolas', 10)).pack(side='right', padx=10)

        # Progres
        self.progress = ttk.Progressbar(self, orient='horizontal', length=1000, mode='determinate')
        self.progress.pack(padx=10, pady=5, fill='x')

        # Tabel
        cols = ('score', 'key', 'len', 'preview')
        self.tree = ttk.Treeview(self, columns=cols, show='headings', height=18)
        self.tree.heading('score', text='SCOR')
        self.tree.heading('key', text='CHEIE')
        self.tree.heading('len', text='LUNGIME')
        self.tree.heading('preview', text='CONȚINUT DETECTAT')
        
        self.tree.column('score', width=80, anchor='center')
        self.tree.column('key', width=150, anchor='w')
        self.tree.column('len', width=80, anchor='center')
        self.tree.column('preview', width=600, anchor='w')
        self.tree.pack(padx=10, pady=5, fill='both', expand=True)

    def browse(self):
        p = filedialog.askopenfilename()
        if p: self.ent_file.delete(0,'end'); self.ent_file.insert(0,p)

    def _poll_ui_updates(self):
        """ Funcție care rulează pe thread-ul principal și preia date din worker """
        if self.candidates_buffer:
            # Golim bufferul și actualizăm tabelul
            batch = self.candidates_buffer[:]
            self.candidates_buffer.clear()
            
            for sc, k, l, txt in batch:
                # Inserăm la începutul tabelului (cele mai noi sus)
                self.tree.insert('', 0, values=(f"{sc:.4f}", k, l, txt))
                # Păstrăm curat (max 500 intrări în tabel vizual)
                if len(self.tree.get_children()) > 500:
                    self.tree.delete(self.tree.get_children()[-1])
        
        self.after(100, self._poll_ui_updates) # Verifică la fiecare 100ms

    def start(self):
        if self._worker and self._worker.is_alive(): return
        
        path = self.ent_file.get()
        try:
            k_min = int(self.ent_kmin.get()); k_max = int(self.ent_kmax.get())
            c_min = int(self.ent_cmin.get()); c_max = int(self.ent_cmax.get())
        except:
            messagebox.showerror("Eroare", "Verifică numerele introduse."); return

        bits = []
        try:
            with open(path, 'r') as f:
                content = f.read()
                bits = [1 if c=='1' else 0 for c in content if c in '01']
        except:
             messagebox.showerror("Eroare", "Nu pot citi fișierul."); return

        if not bits: messagebox.showerror("Eroare", "Fișier fără biți."); return

        # Reset
        self._stop_flag.clear()
        for row in self.tree.get_children(): self.tree.delete(row)
        self.progress['value'] = 0
        
        mode = self.search_mode.get()
        
        # Calcul total pași estimativ
        total_steps = 0
        if k_min > k_max: k_min, k_max = k_max, k_min
        for k in range(k_min, k_max+1): total_steps += (2**k) * (c_max - c_min + 1)
        self.progress['maximum'] = total_steps

        self._worker = threading.Thread(target=self._worker_logic, 
                                        args=(bits, k_min, k_max, c_min, c_max, mode), daemon=True)
        self._worker.start()

    def cancel(self):
        if self._worker: self._stop_flag.set()

    def _worker_logic(self, bits, k_min, k_max, c_min, c_max, mode):
        count = 0
        
        for keylen in range(k_min, k_max + 1):
            if self._stop_flag.is_set(): break
            
            for k_tuple in itertools.product('01', repeat=keylen):
                if self._stop_flag.is_set(): break
                key_str = "".join(k_tuple)
                
                # Update Live Display (doar la fiecare 10 încercări pt performanță)
                if count % 10 == 0:
                    self.var_live_key.set(f"Cheie: {key_str} [L:{keylen}]")
                
                for msg_len in range(c_min, c_max + 1):
                    nbits_needed = msg_len * 8
                    if nbits_needed > len(bits): break
                    
                    # Extrage
                    extracted_bits = extract_from_stego(bits, key_str, nbits_needed)
                    
                    # Analizează în funcție de modul selectat
                    score = 0
                    preview = ""
                    
                    if mode == 'text':
                        score, preview = score_text_candidate(extracted_bits)
                        # Prag filtrare text:
                        if score > 0.65: # Afișăm doar ce pare promițător
                            self.candidates_buffer.append((score, key_str, msg_len, preview))
                            
                    elif mode == 'pattern':
                        score, preview = score_interference_pattern(extracted_bits)
                        # Prag filtrare pattern (0.5 e random, vrem > 0.6 sau < 0.4 adica deviatie mare)
                        if score > 0.15: # Deviatie de la random
                            self.candidates_buffer.append((score, key_str, msg_len, preview))
                    
                    if count % 10 == 0:
                        self.var_live_score.set(f"Scor curent: {score:.4f}")
                        
                    count += 1
                    if count % 50 == 0:
                        self.progress['value'] = count

        self.var_live_key.set("STOPPED / FINISHED")

if __name__ == '__main__':
    app = BruteGUI()
    app.mainloop()
