#!/usr/bin/env python3
"""
Stego GUI pentru embed/extract într-un fișier de biți aleatori (datebinare).
- Fișier input implicit pentru embed: datebinare.txt (conține doar 0/1)
- Fișier output implicit pentru embed: datecriptate.txt (conține header + bitstring)
- Pentru extract: încarcă datecriptate.txt și introduce cheia pentru decodare.
Folosiți o cheie puternică; algoritmul folosește SHA-256 pentru keystream și selecție deterministă de poziții.
Autor: ChatGPT (script generat la cererea utilizatorului)
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import hashlib, os, sys

# --- utilitare bit/bytes ---
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 str_to_bits(s: str, encoding='utf-8'):
    return bytes_to_bits(s.encode(encoding))

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')

# --- SHA256 keystream generator ---
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()  # 256 bits per block
        out_bits.extend(bytes_to_bits(block))
        counter += 1
    return out_bits[:nbits]

# --- deterministc selection of k positions from range(n) using Fisher-Yates partial shuffle ---
def deterministic_sample_positions(key: bytes, n: int, k: int):
    if k > n:
        raise ValueError("k > n")
    indices = list(range(n))
    counter = 0
    # shuffle last k elements
    for i in range(n-1, n-k-1, -1):
        h = hashlib.sha256()
        h.update(key)
        h.update(counter.to_bytes(8,'big'))
        rnd = int.from_bytes(h.digest()[:8], 'big')
        j = rnd % (i+1)
        indices[i], indices[j] = indices[j], indices[i]
        counter += 1
    return indices[-k:]

# --- embed: encrypt message with keystream, then write payload_bits into selected positions (replace) ---
def embed_into_noise(noise_bits, message, key):
    n = len(noise_bits)
    msg_bits = str_to_bits(message)
    m = len(msg_bits)
    if m == 0:
        raise ValueError("Mesaj gol.")
    if m > n:
        raise ValueError("Mesajul e prea lung pentru fisierul de zgomot.")
    keyb = key.encode('utf-8')
    ks = sha256_stream(keyb + b'::ks::msg', m)
    payload = [mb ^ kb for mb,kb in zip(msg_bits, ks)]
    positions = deterministic_sample_positions(keyb + b'::pos', n, m)
    stego = noise_bits.copy()
    for pos, pbit in zip(positions, payload):
        stego[pos] = pbit  # REPLACE
    # header: STEGO1|m=<m>
    header = f"STEGO1|m={m}|hk={hashlib.sha256(keyb).hexdigest()}\\n"
    return header, stego

def extract_from_stego(stego_bits, key, m):
    n = len(stego_bits)
    if m > n:
        raise ValueError("Lungimea mesajului (m) > n")
    keyb = key.encode('utf-8')
    positions = deterministic_sample_positions(keyb + b'::pos', n, m)
    payload = [stego_bits[pos] for pos in positions]
    ks = sha256_stream(keyb + b'::ks::msg', m)
    msg_bits = [p ^ k for p,k in zip(payload, ks)]
    return bits_to_str(msg_bits)

# --- file helpers ---
def read_bitfile(path):
    with open(path, 'r') as f:
        s = f.read().strip()
    if any(c not in '01' for c in s):
        raise ValueError("Fișierul trebuie sa conțină numai 0 și 1.")
    return [int(c) for c in s]

def save_stego_file(path, header, bits):
    with open(path, 'w') as f:
        f.write(header)
        f.write(''.join(str(b) for b in bits))

def parse_stego_file(path):
    with open(path, 'r') as f:
        first = f.readline().strip()
        rest = f.read().strip()
    if not first.startswith("STEGO1|"):
        raise ValueError("Header invalid sau fișier necompatibil.")
    parts = {}
    for p in first.split('|')[1:]:
        if '=' in p:
            k,v = p.split('=',1)
            parts[k]=v
    if 'm' not in parts:
        raise ValueError("Header nu conține lungimea mesajului 'm'.")
    m = int(parts['m'])
    hk = parts.get('hk','')
    if any(c not in '01' for c in rest):
        raise ValueError("Corpul fișierului are caractere invalide.")
    bits = [int(c) for c in rest]
    return m, hk, bits

# --- GUI ---
class StegoGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Stego în zgomot cuantic - Embed / Extract")
        self.geometry("720x520")
        # Embed frame
        frm_e = tk.LabelFrame(self, text="Embed / Ascunde mesaj", padx=8, pady=8)
        frm_e.pack(fill='both', expand=False, padx=8, pady=6)
        tk.Label(frm_e, text="Fișier zgomot (implicit datebinare.txt):").grid(row=0, column=0, sticky='w')
        self.ent_noise = tk.Entry(frm_e, width=60)
        self.ent_noise.grid(row=0, column=1, padx=6)
        self.ent_noise.insert(0, "datebinare.txt")
        tk.Button(frm_e, text="Browse", command=self.browse_noise).grid(row=0, column=2)
        tk.Label(frm_e, text="Mesaj text de ascuns:").grid(row=1, column=0, sticky='nw')
        self.txt_msg = scrolledtext.ScrolledText(frm_e, width=60, height=6)
        self.txt_msg.grid(row=1, column=1, columnspan=2, pady=6)
        tk.Label(frm_e, text="Cheie:").grid(row=2, column=0, sticky='w')
        self.ent_key_e = tk.Entry(frm_e, width=40, show='*')
        self.ent_key_e.grid(row=2, column=1, sticky='w')
        tk.Button(frm_e, text="Embed -> datecriptate.txt", command=self.do_embed).grid(row=3, column=1, sticky='w', pady=6)
        # Extract frame
        frm_d = tk.LabelFrame(self, text="Extract / Dezvăluie mesaj", padx=8, pady=8)
        frm_d.pack(fill='both', expand=False, padx=8, pady=6)
        tk.Label(frm_d, text="Fișier stego (implicit datecriptate.txt):").grid(row=0, column=0, sticky='w')
        self.ent_stego = tk.Entry(frm_d, width=60)
        self.ent_stego.grid(row=0, column=1, padx=6)
        self.ent_stego.insert(0, "datecriptate.txt")
        tk.Button(frm_d, text="Browse", command=self.browse_stego).grid(row=0, column=2)
        tk.Label(frm_d, text="Cheie:").grid(row=1, column=0, sticky='w')
        self.ent_key_d = tk.Entry(frm_d, width=40, show='*')
        self.ent_key_d.grid(row=1, column=1, sticky='w')
        tk.Label(frm_d, text="Lungime mesaj (biți) - opțional (dacă header valid, se completează automat):").grid(row=2, column=0, sticky='w')
        self.ent_m = tk.Entry(frm_d, width=20)
        self.ent_m.grid(row=2, column=1, sticky='w')
        tk.Button(frm_d, text="Extractează mesaj", command=self.do_extract).grid(row=3, column=1, sticky='w', pady=6)
        tk.Label(self, text="Rezultat / Log:").pack(anchor='w', padx=8)
        self.txt_out = scrolledtext.ScrolledText(self, width=88, height=10)
        self.txt_out.pack(padx=8, pady=6)
    
    def log(self, s):
        self.txt_out.insert('end', s + "\n")
        self.txt_out.see('end')
    
    def browse_noise(self):
        p = filedialog.askopenfilename(title="Selectează fișierul de zgomot", filetypes=[("Text files","*.txt"),("All","*.*")])
        if p:
            self.ent_noise.delete(0,'end'); self.ent_noise.insert(0,p)
    
    def browse_stego(self):
        p = filedialog.askopenfilename(title="Selectează fișierul stego", filetypes=[("Text files","*.txt"),("All","*.*")])
        if p:
            self.ent_stego.delete(0,'end'); self.ent_stego.insert(0,p)
            try:
                m,hk,bits = parse_stego_file(p)
                self.ent_m.delete(0,'end'); self.ent_m.insert(0,str(m))
                self.log(f"Header găsit: m={m}, hk={hk}.")
            except Exception as e:
                self.log(f"Nu s-a putut citi header: {e}")
    
    def do_embed(self):
        noise_path = self.ent_noise.get().strip() or "datebinare.txt"
        if not os.path.exists(noise_path):
            messagebox.showerror("Eroare", f"Fișier '{noise_path}' nu găsit.")
            return
        try:
            noise_bits = read_bitfile(noise_path)
        except Exception as e:
            messagebox.showerror("Eroare", str(e)); return
        msg = self.txt_msg.get('1.0','end').strip()
        key = self.ent_key_e.get().strip()
        if not key:
            messagebox.showerror("Eroare", "Introduceți o cheie.")
            return
        try:
            header, stego = embed_into_noise(noise_bits, msg, key)
        except Exception as e:
            messagebox.showerror("Eroare la embed: ", str(e)); return
        out = "datecriptate.txt"
        save_stego_file(out, header, stego)
        self.log(f"Embed realizat cu succes. Fișier salvat: {out}. Header: {header.strip()}")
        messagebox.showinfo("Gata", f"Embed realizat. Fișier salvat: {out}")
    
    def do_extract(self):
        stego_path = self.ent_stego.get().strip() or "datecriptate.txt"
        if not os.path.exists(stego_path):
            messagebox.showerror("Eroare", f"Fișier '{stego_path}' nu găsit."); return
        key = self.ent_key_d.get().strip()
        if not key:
            messagebox.showerror("Eroare", "Introduceți cheia.")
            return
        # parse file header if present
        try:
            m_header, hk, stego_bits = parse_stego_file(stego_path)
            m = m_header
            self.log(f"Header citit: m={m}, hk={hk}")
        except Exception as e:
            # If can't parse header, read user's provided m
            try:
                m = int(self.ent_m.get().strip())
            except Exception:
                messagebox.showerror("Eroare", f"Nu pot determina lungimea mesajului din header și câmpul 'm' nu e completat: {e}")
                return
            try:
                with open(stego_path,'r') as f:
                    # assume file contains only bits (no header)
                    s = f.read().strip()
                    if any(c not in '01' for c in s):
                        raise ValueError("Fișier nevalid (caractere non 0/1).")
                    stego_bits = [int(c) for c in s]
            except Exception as e2:
                messagebox.showerror("Eroare la citire stego:", str(e2)); return
        try:
            msg = extract_from_stego(stego_bits, key, m)
        except Exception as e:
            messagebox.showerror("Eroare la extragere:", str(e)); return
        self.log("Mesaj extras:")
        self.log(msg)
        messagebox.showinfo("Mesaj extras", f"Mesaj extras:\\n{msg}")

def main():
    app = StegoGUI()
    app.mainloop()

if __name__ == '__main__':
    main()
