feat: add cryptography fundamentals lab scripts

This commit is contained in:
Matias Tillerias
2026-09-28 19:56:21 -03:00
commit dbc5d68bc9
6 changed files with 1913 additions and 0 deletions

511
XOR_visualizer.py Normal file
View File

@@ -0,0 +1,511 @@
"""
╔══════════════════════════════════════════════════════════╗
║ 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()