""" ╔══════════════════════════════════════════════════════════╗ ║ XOR Bit Visualizer — Fundamentos de Criptografía ║ ║ Herramienta pedagógica interactiva ║ ╚══════════════════════════════════════════════════════════╝ Requiere Python 3.x (tkinter incluido en la stdlib) Ejecutar: python xor_visualizer.py """ import tkinter as tk from tkinter import ttk import threading import time # ── Paleta de colores ───────────────────────────────────────────────────────── C = { "bg": "#0d0d1a", "panel": "#13132b", "panel2": "#1a1a35", "border": "#2a2a50", "text": "#dde1f0", "muted": "#555580", "a_col": "#38bdf8", # color para operando A (azul cielo) "b_col": "#fb923c", # color para operando B / clave (naranja) "r_col": "#4ade80", # color para resultado (verde) "bit1_bg": "#312e81", # fondo bit = 1 (índigo) "bit0_bg": "#1e1e3a", # fondo bit = 0 "flip_bg": "#7f1d1d", # fondo bit que cambió (rojo oscuro) "same_bg": "#14532d", # fondo bit sin cambio (verde oscuro) "flip_fg": "#fca5a5", # texto bit que cambió "same_fg": "#86efac", # texto bit sin cambio "accent": "#818cf8", # violeta "btn": "#4f46e5", "btn_ok": "#15803d", "btn_warn": "#b45309", } MONO = ("Consolas", 10) MONO_LG = ("Consolas", 13, "bold") MONO_XL = ("Consolas", 18, "bold") # ── Utilidades binarias ─────────────────────────────────────────────────────── def to_bin_grouped(text: str) -> str: """Convierte texto a binario, un byte por carácter, separados por espacio.""" return " ".join(format(ord(c), "08b") for c in text) def xor_with_key(text: str, key: str) -> str: """XOR del texto con la clave (clave cíclica si es más corta).""" return "".join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(text)) # ── Clase principal ─────────────────────────────────────────────────────────── class XORVisualizer: def __init__(self, root: tk.Tk): self.root = root self.root.title("XOR Bit Visualizer — Criptografía") self.root.configure(bg=C["bg"]) self.root.geometry("1180x720") self.root.minsize(900, 600) self._animating = False self._current_result = "" # resultado guardado para descifrado demo self._build_ui() # Valores iniciales self.var_a.set("Hola") self.var_k.set("K") # ── Construcción de la interfaz ─────────────────────────────────────────── def _build_ui(self): self._build_title() # Zona central (3 paneles I/O + bit-viz) center = tk.Frame(self.root, bg=C["bg"]) center.pack(fill=tk.BOTH, expand=True, padx=14, pady=(0, 8)) self._build_io_row(center) self._build_bitviz(center) self._build_bottom(center) # ── Barra de título ─────────────────────────────────────────────────────── def _build_title(self): bar = tk.Frame(self.root, bg=C["panel"], pady=10) bar.pack(fill=tk.X) tk.Label(bar, text="⊕ XOR Bit Visualizer", font=("Consolas", 15, "bold"), bg=C["panel"], fg=C["accent"]).pack(side=tk.LEFT, padx=18) tk.Label(bar, text="Herramienta Pedagógica · Fundamentos de Criptografía", font=MONO, bg=C["panel"], fg=C["muted"]).pack(side=tk.RIGHT, padx=18) # ── Fila superior: A | XOR-op | B | → | Resultado ───────────────────────── def _build_io_row(self, parent): row = tk.Frame(parent, bg=C["bg"]) row.pack(fill=tk.X, pady=(10, 6)) # ---- Panel A ---- self.var_a = tk.StringVar() self.bin_a = self._io_panel(row, "OPERANDO A (texto plano)", self.var_a, C["a_col"], editable=True) self.var_a.trace_add("write", self._on_a_change) # ---- Símbolo ⊕ ---- op = tk.Frame(row, bg=C["bg"]) op.pack(side=tk.LEFT, padx=4) tk.Label(op, text="⊕", font=("Consolas", 30, "bold"), bg=C["bg"], fg=C["accent"]).pack(expand=True) # ---- Panel K (clave) ---- self.var_k = tk.StringVar() self.bin_k = self._io_panel(row, "CLAVE B (key — cíclica)", self.var_k, C["b_col"], editable=True) self.var_k.trace_add("write", self._on_k_change) # ---- Botones centrales ---- btn_col = tk.Frame(row, bg=C["bg"], padx=6) btn_col.pack(side=tk.LEFT) self.btn_xor = self._btn(btn_col, "Cifrar A ⊕ B →", C["btn"], self._start_xor) self.btn_xor.pack(fill=tk.X, pady=(0, 6)) self.btn_dec = self._btn(btn_col, "⟳ Descifrar (R ⊕ B)", C["btn_warn"], self._start_decrypt) self.btn_dec.pack(fill=tk.X, pady=(0, 6)) self.btn_dec.config(state=tk.DISABLED) self._btn(btn_col, "Reset", C["muted"], self._reset).pack(fill=tk.X) # ---- Panel Resultado ---- self.var_r = tk.StringVar() self.bin_r = self._io_panel(row, "RESULTADO A ⊕ B", self.var_r, C["r_col"], editable=False) # ── Panel I/O genérico ──────────────────────────────────────────────────── def _io_panel(self, parent, title, str_var, color, editable=True): outer = tk.Frame(parent, bg=color, padx=2, pady=2) outer.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5) inner = tk.Frame(outer, bg=C["panel"], padx=10, pady=8) inner.pack(fill=tk.BOTH, expand=True) # Cabecera tk.Label(inner, text=title, font=("Consolas", 9, "bold"), bg=C["panel"], fg=color).pack(anchor=tk.W) # ASCII tk.Label(inner, text="ASCII", font=("Consolas", 8), bg=C["panel"], fg=C["muted"]).pack(anchor=tk.W, pady=(6, 0)) entry = tk.Entry(inner, textvariable=str_var, font=MONO_LG, bg=C["bg"], fg=color, insertbackground=color, relief=tk.FLAT, bd=4, state=tk.NORMAL if editable else tk.DISABLED) entry.pack(fill=tk.X, pady=(2, 8)) # Binario tk.Label(inner, text="BINARIO (8 bits / carácter)", font=("Consolas", 8), bg=C["panel"], fg=C["muted"]).pack(anchor=tk.W) bin_box = tk.Text(inner, font=("Consolas", 9), bg=C["bg"], fg=color, relief=tk.FLAT, bd=4, height=3, wrap=tk.WORD, state=tk.DISABLED) bin_box.pack(fill=tk.BOTH, pady=(2, 0)) return bin_box # retornamos para actualizarlo desde código # ── Visualización bit a bit ─────────────────────────────────────────────── def _build_bitviz(self, parent): frame = tk.LabelFrame(parent, text=" Visualización bit a bit ", font=("Consolas", 9, "bold"), bg=C["panel2"], fg=C["accent"], relief=tk.FLAT, highlightbackground=C["border"], highlightthickness=1) frame.pack(fill=tk.BOTH, expand=True, pady=(4, 6)) self.canvas = tk.Canvas(frame, bg=C["panel2"], height=130, highlightthickness=0) hbar = ttk.Scrollbar(frame, orient=tk.HORIZONTAL, command=self.canvas.xview) self.canvas.configure(xscrollcommand=hbar.set) hbar.pack(side=tk.BOTTOM, fill=tk.X) self.canvas.pack(fill=tk.BOTH, expand=True, padx=8, pady=4) # Placeholder inicial self.canvas.create_text(20, 65, anchor=tk.W, text="Escribe texto y clave, luego presiona «Cifrar A ⊕ B»", font=MONO, fill=C["muted"]) # ── Zona inferior: tabla de verdad + propiedades + leyenda ──────────────── def _build_bottom(self, parent): row = tk.Frame(parent, bg=C["bg"]) row.pack(fill=tk.X) self._build_truth_table(row) self._build_properties(row) self._build_legend(row) def _build_truth_table(self, parent): frame = tk.LabelFrame(parent, text=" Tabla de Verdad XOR ", font=("Consolas", 9, "bold"), bg=C["panel2"], fg=C["accent"], relief=tk.FLAT) frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 8)) headers = ["A", "B", "A ⊕ B"] for j, h in enumerate(headers): tk.Label(frame, text=h, font=("Consolas", 10, "bold"), bg=C["panel2"], fg=C["accent"], padx=14, pady=4).grid(row=0, column=j) rows = [("0", "0", "0"), ("0", "1", "1"), ("1", "0", "1"), ("1", "1", "0")] for i, (a, b, r) in enumerate(rows, 1): for j, val in enumerate([a, b]): tk.Label(frame, text=val, font=("Consolas", 11), bg=C["panel2"], fg=C["text"], padx=14, pady=3).grid(row=i, column=j) fg = C["r_col"] if r == "1" else C["muted"] tk.Label(frame, text=r, font=("Consolas", 11, "bold"), bg=C["panel2"], fg=fg, padx=14, pady=3).grid(row=i, column=2) def _build_properties(self, parent): frame = tk.LabelFrame(parent, text=" Propiedades algebraicas del XOR ", font=("Consolas", 9, "bold"), bg=C["panel2"], fg=C["accent"], relief=tk.FLAT) frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 8)) props = [ ("Identidad", "A ⊕ 0 = A"), ("Auto-inverso", "A ⊕ A = 0"), ("Conmutatividad", "A ⊕ B = B ⊕ A"), ("Asociatividad", "(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)"), ("★ Descifrado", "(A ⊕ B) ⊕ B = A ← ¡fundamento del cifrado!"), ] for i, (name, expr) in enumerate(props): fg_name = C["b_col"] if "★" in name else C["muted"] tk.Label(frame, text=f"{name}:", font=("Consolas", 9, "bold"), bg=C["panel2"], fg=fg_name, anchor=tk.W, padx=10, pady=3).grid(row=i, column=0, sticky=tk.W) tk.Label(frame, text=expr, font=("Consolas", 9), bg=C["panel2"], fg=C["accent"], anchor=tk.W, padx=6, pady=3).grid(row=i, column=1, sticky=tk.W) def _build_legend(self, parent): frame = tk.LabelFrame(parent, text=" Leyenda bits ", font=("Consolas", 9, "bold"), bg=C["panel2"], fg=C["accent"], relief=tk.FLAT) frame.pack(side=tk.LEFT, fill=tk.Y) items = [ (C["bit1_bg"], C["text"], "■ Bit = 1"), (C["bit0_bg"], C["muted"], "■ Bit = 0"), (C["flip_bg"], C["flip_fg"], "■ Bit modificado (XOR=1)"), (C["same_bg"], C["same_fg"], "■ Bit sin cambio (XOR=0)"), ] for i, (bg, fg, label) in enumerate(items): tk.Label(frame, text=label, font=("Consolas", 9), bg=bg, fg=fg, padx=10, pady=5, anchor=tk.W).pack(fill=tk.X, padx=6, pady=2) # ── Helpers ─────────────────────────────────────────────────────────────── @staticmethod def _btn(parent, text, color, cmd): return tk.Button(parent, text=text, font=("Consolas", 9, "bold"), bg=color, fg="white", relief=tk.FLAT, padx=10, pady=6, cursor="hand2", command=cmd) def _set_text_widget(self, widget, text): widget.config(state=tk.NORMAL) widget.delete("1.0", tk.END) widget.insert("1.0", text) widget.config(state=tk.DISABLED) def _set_entry(self, widget, var, text): widget.config(state=tk.NORMAL) var.set(text) widget.config(state=tk.DISABLED) # ── Callbacks de cambio en entradas ────────────────────────────────────── def _on_a_change(self, *_): self._set_text_widget(self.bin_a, to_bin_grouped(self.var_a.get())) self._clear_result() def _on_k_change(self, *_): self._set_text_widget(self.bin_k, to_bin_grouped(self.var_k.get())) self._clear_result() def _clear_result(self): self._set_text_widget(self.bin_r, "") self.var_r.set("") self._current_result = "" self.canvas.delete("all") self.canvas.create_text(20, 65, anchor=tk.W, text="Escribe texto y clave, luego presiona «Cifrar A ⊕ B»", font=MONO, fill=C["muted"]) self.btn_dec.config(state=tk.DISABLED) def _reset(self): if self._animating: return self.var_a.set("") self.var_k.set("") self._clear_result() # ── Lógica de animación XOR ─────────────────────────────────────────────── def _start_xor(self): if self._animating: return text = self.var_a.get() key = self.var_k.get() if not text or not key: self._flash_warning("⚠ Escribe texto y clave antes de aplicar XOR") return self._run_animation(text, key, result_target_var=self.var_r, result_target_bin=self.bin_r, store_as_result=True) def _start_decrypt(self): """Demuestra que (A⊕B)⊕B = A (propiedad de descifrado).""" if self._animating or not self._current_result: return key = self.var_k.get() if not key: return self._run_animation(self._current_result, key, result_target_var=self.var_a, result_target_bin=self.bin_a, store_as_result=False, decrypt_mode=True) def _run_animation(self, text, key, *, result_target_var, result_target_bin, store_as_result, decrypt_mode=False): self._animating = True self.btn_xor.config(state=tk.DISABLED, text="Calculando…") self.btn_dec.config(state=tk.DISABLED) t = threading.Thread( target=self._animate_worker, args=(text, key, result_target_var, result_target_bin, store_as_result, decrypt_mode), daemon=True ) t.start() def _animate_worker(self, text, key, var_out, bin_out, store_as_result, decrypt_mode): partial_result = "" bytes_a, bytes_b, bytes_r = [], [], [] for i, ch in enumerate(text): k = key[i % len(key)] a_byte = format(ord(ch), "08b") b_byte = format(ord(k), "08b") r_char = chr(ord(ch) ^ ord(k)) r_byte = format(ord(r_char), "08b") partial_result += r_char bytes_a.append(a_byte) bytes_b.append(b_byte) bytes_r.append(r_byte) time.sleep(0.3) pr = partial_result # captura para closure ba = list(bytes_a) bb = list(bytes_b) br = list(bytes_r) self.root.after(0, lambda pr=pr, ba=ba, bb=bb, br=br: self._update_result(pr, ba, bb, br, var_out, bin_out)) self.root.after(0, lambda: self._finish(partial_result, store_as_result, decrypt_mode)) def _update_result(self, partial, bytes_a, bytes_b, bytes_r, var_out, bin_out): # ASCII display (editable temporalmente) bin_out.config(state=tk.NORMAL) bin_out.delete("1.0", tk.END) bin_out.insert("1.0", " ".join(bytes_r)) bin_out.config(state=tk.DISABLED) printable = "".join( c if 32 <= ord(c) < 127 else f"[{ord(c):02X}]" for c in partial ) var_out.set(printable) self._draw_bits(bytes_a, bytes_b, bytes_r) def _finish(self, result, store_as_result, decrypt_mode): self._animating = False self.btn_xor.config(state=tk.NORMAL, text="Cifrar A ⊕ B →") if store_as_result: self._current_result = result self.btn_dec.config(state=tk.NORMAL) else: # Fin del descifrado: restaurar texto original en var_a printable = "".join( c if 32 <= ord(c) < 127 else f"[{ord(c):02X}]" for c in result ) self.var_a.set(printable) self._set_text_widget(self.bin_a, to_bin_grouped(result)) if self._current_result: self.btn_dec.config(state=tk.NORMAL) # ── Dibujo del canvas bit a bit ─────────────────────────────────────────── def _draw_bits(self, bytes_a, bytes_b, bytes_r): cv = self.canvas cv.delete("all") CW, CH = 20, 26 # ancho y alto de cada celda de bit GAP = 2 # espacio entre celdas SEP = 8 # separador entre bytes ROW_GAP = 6 # espacio entre filas OX, OY = 48, 6 # origen row_meta = [ ("A", C["a_col"]), ("B", C["b_col"]), ("⊕", C["r_col"]), ] # Etiquetas de fila for r, (label, color) in enumerate(row_meta): y_ctr = OY + r * (CH + ROW_GAP) + CH // 2 cv.create_text(OX - 10, y_ctr, text=label, anchor=tk.E, font=("Consolas", 9, "bold"), fill=color) for ci, (ba, bb, br) in enumerate(zip(bytes_a, bytes_b, bytes_r)): byte_x = OX + ci * (8 * (CW + GAP) + SEP) for bi in range(8): x = byte_x + bi * (CW + GAP) a_bit = ba[bi]; b_bit = bb[bi]; r_bit = br[bi] bits = [a_bit, b_bit, r_bit] for ri, bit in enumerate(bits): y = OY + ri * (CH + ROW_GAP) label_color = row_meta[ri][1] if ri < 2: # Operandos A y B bg = C["bit1_bg"] if bit == "1" else C["bit0_bg"] fg = label_color if bit == "1" else C["muted"] else: # Resultado — resaltar si el bit cambió flipped = (a_bit != r_bit) if flipped: bg = C["flip_bg"] fg = C["flip_fg"] else: bg = C["same_bg"] fg = C["same_fg"] cv.create_rectangle(x, y, x + CW, y + CH, fill=bg, outline=C["bg"], width=1) cv.create_text(x + CW // 2, y + CH // 2, text=bit, font=("Consolas", 9, "bold"), fill=fg) # Separador entre bytes (línea vertical) if ci < len(bytes_a) - 1: sep_x = byte_x + 8 * (CW + GAP) + SEP // 2 cv.create_line(sep_x, OY, sep_x, OY + 3 * (CH + ROW_GAP) - ROW_GAP, fill=C["border"], width=1, dash=(3, 3)) # Etiqueta del carácter encima char_label_x = byte_x + 4 * (CW + GAP) - CW // 2 # (espacio muy limitado; omitimos para no aglomerar) # Actualizar scrollregion total_w = OX + len(bytes_a) * (8 * (CW + GAP) + SEP) + 20 cv.configure(scrollregion=(0, 0, total_w, 130)) # ── Flash de advertencia ────────────────────────────────────────────────── def _flash_warning(self, msg): win = tk.Toplevel(self.root) win.title("") win.configure(bg="#7f1d1d") win.resizable(False, False) tk.Label(win, text=msg, font=MONO, bg="#7f1d1d", fg="#fca5a5", padx=24, pady=14).pack() win.after(2000, win.destroy) win.grab_set() # ── Punto de entrada ────────────────────────────────────────────────────────── if __name__ == "__main__": root = tk.Tk() app = XORVisualizer(root) # Centrar la ventana en pantalla root.update_idletasks() w, h = root.winfo_width(), root.winfo_height() sw = root.winfo_screenwidth() sh = root.winfo_screenheight() root.geometry(f"{w}x{h}+{(sw - w)//2}+{(sh - h)//2}") root.mainloop()