#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Brute-force GUI + raport pentru detectare mesaje ascunse în 'datecriptate.txt'
- Implicit: keylen=3, max_chars=50, topN=30
- La final scriptul salvează raportul 'bruteforce_report.txt' în directorul curent
Autor: ChatGPT (adaptare cerută de utilizator)
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
import hashlib, os, threading, time, itertools

# ----------------- 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 (aceeași metodă folosită la embed) -----------------
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 ']

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 chei binare — detectare mesaje ascunse + raport")
        self.geometry("960x700")
        frm = tk.Frame(self); frm.pack(fill='x', padx=8, pady=6)
        tk.Label(frm, text="Fișier stego (implicit 'datecriptate.txt'):").grid(row=0,column=0,sticky='w')
        self.ent_file = tk.Entry(frm, width=86); self.ent_file.grid(row=0,column=1); self.ent_file.insert(0,"datecriptate.txt")
        tk.Button(frm, text="Browse", command=self.browse).grid(row=0,column=2)
        tk.Label(frm, text="Lungime cheie (biți) — implicit 3:").grid(row=1,column=0,sticky='w')
        self.ent_keylen = tk.Entry(frm, width=8); self.ent_keylen.grid(row=1,column=1,sticky='w'); self.ent_keylen.insert(0,"3")
        tk.Label(frm, text="Max caractere mesaj încercate (implicit 50):").grid(row=2,column=0,sticky='w')
        self.ent_maxchars = tk.Entry(frm, width=8); self.ent_maxchars.grid(row=2,column=1,sticky='w'); self.ent_maxchars.insert(0,"50")
        tk.Label(frm, text="Top rezultate afișate (implicit 30):").grid(row=3,column=0,sticky='w')
        self.ent_topn = tk.Entry(frm, width=8); self.ent_topn.grid(row=3,column=1,sticky='w'); self.ent_topn.insert(0,"30")
        tk.Button(frm, text="Pornește brute-force", bg='#ffcccc', command=self.start).grid(row=4,column=1, sticky='w', pady=6)
        tk.Button(frm, text="Anulează", command=self.cancel).grid(row=4,column=2, sticky='w', pady=6)

        self.progress = ttk.Progressbar(self, orient='horizontal', length=900, mode='determinate')
        self.progress.pack(padx=8, pady=4)

        cols = ('key','chars','score','pr_ratio','common_words','message_preview')
        self.tree = ttk.Treeview(self, columns=cols, show='headings', height=20)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=120 if c!='message_preview' else 420, anchor='w')
        self.tree.pack(padx=8, pady=6)

        tk.Label(self, text="Log / Detalii:").pack(anchor='w', padx=8)
        self.log = scrolledtext.ScrolledText(self, width=116, height=10); self.log.pack(padx=8, pady=6)

        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', s + "\n"); self.log.see('end')

    def start(self):
        if self._worker and self._worker.is_alive():
            messagebox.showwarning("Atentie", "Brute-force este deja în desfășurare.")
            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:
            keylen = int(self.ent_keylen.get().strip())
            maxchars = int(self.ent_maxchars.get().strip())
            topn = int(self.ent_topn.get().strip())
        except Exception as e:
            messagebox.showerror("Parametrii invalizi", str(e)); return
        if keylen <= 0 or keylen > 24:
            messagebox.showerror("Parametrii invalizi", "Lungimea cheii trebuie între 1 și 24 (recomandat <= 16)."); return
        bits = read_bitfile(path)
        nbits = len(bits)
        if nbits == 0:
            messagebox.showerror("Eroare", "Fișierul nu conține biți 0/1 validi."); return
        for i in self.tree.get_children():
            self.tree.delete(i)
        self.progress['value'] = 0
        total_keys = 2 ** keylen
        self.progress['maximum'] = total_keys * maxchars
        self._stop_flag.clear()
        self.candidates_final = []
        self._worker = threading.Thread(target=self._worker_func, args=(bits, keylen, maxchars, topn, path), daemon=True)
        self._worker.start()
        self.logmsg(f"Start brute-force: keylen={keylen}, maxchars={maxchars}, total_keys={total_keys}.")

    def cancel(self):
        if self._worker and self._worker.is_alive():
            self._stop_flag.set()
            self.logmsg("Stop requested...")
        else:
            self.logmsg("Nu este niciun proces activ.")

    def _worker_func(self, bits, keylen, maxchars, topn, path):
        candidates = []
        checked = 0
        start = time.time()
        for kbits in itertools.product('01', repeat=keylen):
            if self._stop_flag.is_set():
                break
            key = ''.join(kbits)
            for chars in range(1, maxchars+1):
                if self._stop_flag.is_set():
                    break
                m_bits = chars * 8
                if m_bits > len(bits):
                    break
                try:
                    msg = extract_from_stego(bits, key, m_bits)
                except Exception:
                    msg = ""
                pr = printable_ratio(msg)
                cw = common_word_score(msg)
                sc = score_candidate(msg)
                checked += 1
                if checked % 20 == 0:
                    self.progress['value'] = checked
                if pr >= 0.90 or sc > 0.6 or cw >= 1:
                    candidates.append((sc, key, chars, pr, cw, msg))
                    candidates.sort(key=lambda x: x[0], reverse=True)
                    if len(candidates) > topn*5:
                        candidates = candidates[:topn*5]
            if checked % 200 == 0:
                self._update_table(candidates, topn)
        # final
        self._update_table(candidates, topn)
        elapsed = time.time() - start
        self.logmsg(f"Brute-force finalizat. Timp: {elapsed:.2f}s, încercări: {checked}.")
        if self._stop_flag.is_set():
            self.logmsg("Proces oprit de utilizator.")
        else:
            self.logmsg("Proces terminat normal. Verifică rezultatele afișate.")
        # salvează raport
        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
            for sc, key, chars, pr, cw, msg in candidates:
                if shown >= topn: break
                k = (key, chars)
                if k in seen: continue
                seen.add(k)
                preview = msg.replace('\n','\\n')[:300]
                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")
        with open(report_path, 'w', encoding='utf-8') as f:
            f.write("RAPORT BRUTE-FORCE STEGO\n")
            f.write(f"Fișier analizat: {path}\n")
            f.write(f"Timp rulare: {elapsed:.2f}s\n")
            f.write(f"Încercări totale: {attempts}\n\n")
            if not self.candidates_final:
                f.write("REZULTAT: NU A GASIT NIMIC (conform heuristiciilor aplicate)\n")
                self.logmsg("Raport generat: NU A GASIT NIMIC.")
            else:
                f.write(f"REZULTAT: GĂSITE {len(self.candidates_final)} kandidaturi (ord. descrescătoare scor)\n\n")
                for sc, key, chars, pr, cw, msg in self.candidates_final:
                    f.write(f"Key={key} | chars={chars} | score={sc:.4f} | printable={pr:.3f} | common_words={cw}\n")
                    f.write("Preview mesaj (primele 500 caractere):\n")
                    f.write(msg.replace('\n','\\n')[:500] + "\n")
                    f.write("-"*60 + "\n")
                self.logmsg(f"Raport generat: {report_path} (conține {len(self.candidates_final)} candidates)")
        # deschide mesaj final in GUI
        self.logmsg(f"Raport salvat: {report_path}")
        messagebox.showinfo("Raport salvat", f"Raportul a fost salvat: {report_path}")

if __name__ == '__main__':
    app = BruteGUI()
    app.mainloop()
