512 lines
22 KiB
Python
512 lines
22 KiB
Python
"""
|
|
╔══════════════════════════════════════════════════════════╗
|
|
║ 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()
|