From dbc5d68bc94ebf05f79a4e180c9f9732a2ab5c48 Mon Sep 17 00:00:00 2001 From: Matias Tillerias Date: Mon, 28 Sep 2026 19:56:21 -0300 Subject: [PATCH] feat: add cryptography fundamentals lab scripts --- .gitignore | 2 + Lab0_cifrados_clasicos.py | 599 ++++++++++++++++++++++++++++++++++++++ Lab1_XOR.py | 198 +++++++++++++ Lab2_Hashing.py | 88 ++++++ Lab3_ECB_CBC_v2.py | 515 ++++++++++++++++++++++++++++++++ XOR_visualizer.py | 511 ++++++++++++++++++++++++++++++++ 6 files changed, 1913 insertions(+) create mode 100644 .gitignore create mode 100644 Lab0_cifrados_clasicos.py create mode 100644 Lab1_XOR.py create mode 100644 Lab2_Hashing.py create mode 100644 Lab3_ECB_CBC_v2.py create mode 100644 XOR_visualizer.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64701cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.atl/ +__pycache__/ diff --git a/Lab0_cifrados_clasicos.py b/Lab0_cifrados_clasicos.py new file mode 100644 index 0000000..01ec058 --- /dev/null +++ b/Lab0_cifrados_clasicos.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +""" +cifrados_clasicos.py — Tres Cifrados Clásicos en Tabs +Herramienta pedagógica · Fundamentos de Criptografía +Requiere Python 3.x (tkinter incluido en stdlib) +Ejecutar: python cifrados_clasicos.py +""" +import tkinter as tk +from tkinter import ttk +import string, math, random + +ALPHA = string.ascii_uppercase +N = 26 + +# Frecuencias del español (%) +FREQ_ES = { + 'A':12.53,'B':1.42,'C':4.68,'D':5.86,'E':13.68,'F':0.69,'G':1.01, + 'H':0.70,'I':6.25,'J':0.44,'K':0.02,'L':4.97,'M':3.15,'N':6.71, + 'O':8.68,'P':2.51,'Q':0.88,'R':6.87,'S':7.98,'T':4.63,'U':3.93, + 'V':0.90,'W':0.01,'X':0.22,'Y':0.90,'Z':0.52, +} + +P = dict( + bg='#0d0d1a', panel='#13132b', panel2='#1a1a35', + border='#2a2a50', text='#dde1f0', muted='#555580', + accent='#818cf8', ac='#38bdf8', bc='#fb923c', + rc='#4ade80', btn='#4f46e5', hl='#fde68a', +) + + +def mk_btn(parent, label, color, cmd, **kw): + return tk.Button(parent, text=label, font=('Consolas', 9, 'bold'), + bg=color, fg='white', relief=tk.FLAT, + padx=10, pady=5, cursor='hand2', command=cmd, **kw) + + +class CifradosClasicos: + def __init__(self, root): + self.root = root + root.title('Cifrados Clásicos — Fundamentos de Criptografía') + root.configure(bg=P['bg']) + root.geometry('1080x760') + root.minsize(860, 620) + + self.shift_var = tk.IntVar(value=3) + self.cmode_var = tk.StringVar(value='E') + self.vig_plain = tk.StringVar() + self.vig_key = tk.StringVar(value='CLAVE') + self.vig_out = tk.StringVar() + self.sub_mode = tk.StringVar(value='E') + self.sub_key = list(ALPHA) + random.shuffle(self.sub_key) + + # caches + self._vig_p = self._vig_k = self._vig_c = [] + self._sub_result = '' + self._sub_mapping = {} + + self._build() + + # ── Scaffold ────────────────────────────────────────────────────────────── + def _build(self): + bar = tk.Frame(self.root, bg=P['panel'], pady=10) + bar.pack(fill=tk.X) + tk.Label(bar, text='🔐 Cifrados Clásicos', + font=('Consolas', 15, 'bold'), + bg=P['panel'], fg=P['accent']).pack(side=tk.LEFT, padx=18) + tk.Label(bar, + text='César · Vigenère · Sustitución + Análisis de Frecuencia', + font=('Consolas', 9), bg=P['panel'], fg=P['muted'] + ).pack(side=tk.RIGHT, padx=18) + + st = ttk.Style() + st.theme_use('default') + st.configure('C.TNotebook', background=P['bg'], borderwidth=0) + st.configure('C.TNotebook.Tab', + background=P['panel2'], foreground=P['muted'], + padding=[22, 8], font=('Consolas', 10, 'bold')) + st.map('C.TNotebook.Tab', + background=[('selected', P['btn'])], + foreground=[('selected', 'white')]) + + nb = ttk.Notebook(self.root, style='C.TNotebook') + nb.pack(fill=tk.BOTH, expand=True, padx=12, pady=10) + + for tab_text, fn in [ + (' César / ROT-N ', self._build_caesar), + (' Vigenère ', self._build_vigenere), + (' Sustitución + Frecuencias ', self._build_substitution), + ]: + f = tk.Frame(nb, bg=P['bg']) + nb.add(f, text=tab_text) + fn(f) + + # ══════════════════════════════════════════════════════════════════════════ + # TAB 1 · CÉSAR / ROT-N + # ══════════════════════════════════════════════════════════════════════════ + def _build_caesar(self, parent): + left = tk.Frame(parent, bg=P['bg'], padx=16, pady=12) + left.pack(side=tk.LEFT, fill=tk.Y) + right = tk.Frame(parent, bg=P['bg'], pady=12) + right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Slider desplazamiento + tk.Label(left, text='Desplazamiento N', font=('Consolas', 10, 'bold'), + bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) + + srow = tk.Frame(left, bg=P['bg']) + srow.pack(fill=tk.X, pady=(4, 12)) + tk.Scale(srow, from_=0, to=25, orient=tk.HORIZONTAL, + variable=self.shift_var, command=self._on_shift, + bg=P['panel2'], fg=P['text'], highlightthickness=0, + troughcolor=P['border'], length=230, + font=('Consolas', 9)).pack(side=tk.LEFT) + self.c_shift_lbl = tk.Label(srow, text='N = 3', + font=('Consolas', 13, 'bold'), + bg=P['bg'], fg=P['hl'], padx=8) + self.c_shift_lbl.pack(side=tk.LEFT) + + # Modo + mrow = tk.Frame(left, bg=P['bg']) + mrow.pack(fill=tk.X, pady=(0, 12)) + for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]: + tk.Radiobutton(mrow, text=txt, variable=self.cmode_var, value=val, + command=self._update_caesar, + bg=P['bg'], fg=col, selectcolor=P['panel2'], + activebackground=P['bg'], + font=('Consolas', 10, 'bold') + ).pack(side=tk.LEFT, padx=(0, 18)) + + # Input / output + for attr, lbl_txt, fg, editable in [ + ('c_in', 'Texto de entrada', P['ac'], True), + ('c_out', 'Resultado', P['rc'], False), + ]: + tk.Label(left, text=lbl_txt, font=('Consolas', 9), + bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) + w = tk.Text(left, height=4, width=33, + font=('Consolas', 11), wrap=tk.WORD, + bg=P['panel2'] if editable else P['panel'], + fg=fg, relief=tk.FLAT, insertbackground=fg, + state=tk.NORMAL if editable else tk.DISABLED) + w.pack(pady=(2, 10)) + setattr(self, attr, w) + self.c_in.bind('', lambda e: self._update_caesar()) + self.c_in.insert('1.0', 'Hola Mundo') + + self.c_formula = tk.Label(left, text='', font=('Consolas', 9), + bg=P['bg'], fg=P['muted'], justify=tk.LEFT) + self.c_formula.pack(anchor=tk.W) + + # Rueda + tk.Label(right, text='Rueda de César', + font=('Consolas', 10, 'bold'), + bg=P['bg'], fg=P['accent']).pack() + self.c_cv = tk.Canvas(right, width=430, height=430, + bg=P['bg'], highlightthickness=0) + self.c_cv.pack(pady=4) + self._update_caesar() + + def _on_shift(self, val=None): + self.c_shift_lbl.config(text=f'N = {self.shift_var.get()}') + self._update_caesar() + + def _draw_wheel(self, hl_plain=None, hl_cipher=None): + cv = self.c_cv + cv.delete('all') + shift = self.shift_var.get() + cx = cy = 215 + R1, R2 = 190, 145 # outer / inner ring radius + T1, T2 = 172, 126 # text radii + + # Rings + cv.create_oval(cx-R1, cy-R1, cx+R1, cy+R1, + fill=P['panel2'], outline=P['border'], width=2) + cv.create_oval(cx-R2, cy-R2, cx+R2, cy+R2, + fill=P['panel'], outline=P['accent'], width=2) + cv.create_oval(cx-105, cy-105, cx+105, cy+105, + fill=P['panel2'], outline=P['border'], width=1) + + # Segment dividers + for i in range(N): + a = math.radians(-90 + i * 360/N) + xa = cx + R2 * math.cos(a); ya = cy + R2 * math.sin(a) + xb = cx + R1 * math.cos(a); yb = cy + R1 * math.sin(a) + cv.create_line(xa, ya, xb, yb, fill=P['border'], width=1) + + for i in range(N): + angle = math.radians(-90 + i * 360/N) + + # Outer: texto plano (fijo) + plain_letter = ALPHA[i] + xo = cx + T1 * math.cos(angle) + yo = cy + T1 * math.sin(angle) + is_hl = (hl_plain and plain_letter == hl_plain) + if is_hl: + cv.create_oval(xo-14, yo-14, xo+14, yo+14, + fill='#2d2200', outline=P['hl'], width=2) + cv.create_text(xo, yo, text=plain_letter, + font=('Consolas', 11, 'bold'), + fill=P['hl'] if is_hl else P['text']) + + # Inner: cifrado (girado por shift) + cipher_letter = ALPHA[(i + shift) % N] + xi = cx + T2 * math.cos(angle) + yi = cy + T2 * math.sin(angle) + is_hl2 = (hl_cipher and cipher_letter == hl_cipher) + if is_hl2: + cv.create_oval(xi-14, yi-14, xi+14, yi+14, + fill='#0a2510', outline=P['rc'], width=2) + cv.create_text(xi, yi, text=cipher_letter, + font=('Consolas', 11, 'bold'), + fill=P['rc'] if is_hl2 else P['bc']) + + # Centro + cv.create_text(cx, cy - 10, text=f'N = {shift}', + font=('Consolas', 14, 'bold'), fill=P['accent']) + cv.create_text(cx, cy + 12, text='C = (P+N) mod 26', + font=('Consolas', 8), fill=P['muted']) + + # Flecha indicadora en la cima + tip_x = cx + (R1 + 8) * math.cos(math.radians(-90)) + tip_y = cy + (R1 + 8) * math.sin(math.radians(-90)) + cv.create_polygon(tip_x - 7, tip_y, tip_x + 7, tip_y, tip_x, tip_y - 18, + fill=P['hl']) + + # Etiquetas de anillos + cv.create_text(cx, cy - T1 - 22, text='Texto plano (fijo)', + font=('Consolas', 8), fill=P['ac']) + cv.create_text(cx, cy - T2 + 26, text='Cifrado (gira)', + font=('Consolas', 8), fill=P['bc']) + + def _update_caesar(self, *_): + text = self.c_in.get('1.0', tk.END).strip().upper() + shift = self.shift_var.get() + sgn = 1 if self.cmode_var.get() == 'E' else -1 + + result = '' + for ch in text: + result += ALPHA[(ALPHA.index(ch) + sgn * shift) % N] if ch in ALPHA else ch + + self.c_out.config(state=tk.NORMAL) + self.c_out.delete('1.0', tk.END) + self.c_out.insert('1.0', result) + self.c_out.config(state=tk.DISABLED) + + first_p = next((c for c in text if c in ALPHA), None) + first_c = ALPHA[(ALPHA.index(first_p) + sgn * shift) % N] if first_p else None + + if self.cmode_var.get() == 'E': + self.c_formula.config( + text=f'Fórmula: C = (P + {shift}) mod 26\n' + f'Ejemplo: A(0) → {ALPHA[shift % N]}({shift})') + else: + self.c_formula.config( + text=f'Fórmula: P = (C − {shift}) mod 26\n' + f'Ejemplo: {ALPHA[shift % N]}({shift}) → A(0)') + + self._draw_wheel(hl_plain=first_p, hl_cipher=first_c) + + # ══════════════════════════════════════════════════════════════════════════ + # TAB 2 · VIGENÈRE + # ══════════════════════════════════════════════════════════════════════════ + def _build_vigenere(self, parent): + top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10) + top.pack(fill=tk.X) + + # IO + io = tk.Frame(top, bg=P['bg']) + io.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18)) + + for attr, lbl_txt, var, fg, editable in [ + ('v_plain_e', 'Texto plano', self.vig_plain, P['ac'], True), + ('v_key_e', 'Clave', self.vig_key, P['bc'], True), + ('v_out_e', 'Cifrado', self.vig_out, P['rc'], False), + ]: + tk.Label(io, text=lbl_txt, font=('Consolas', 9), + bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) + e = tk.Entry(io, textvariable=var, + font=('Consolas', 12, 'bold'), relief=tk.FLAT, + bg=P['panel2'] if editable else P['panel'], + fg=fg, insertbackground=fg, width=26, + state=tk.NORMAL if editable else tk.DISABLED) + e.pack(pady=(2, 8), fill=tk.X) + setattr(self, attr, e) + + self.vig_plain.trace_add('write', self._update_vig) + self.vig_key.trace_add('write', self._update_vig) + self.vig_plain.set('HOLA') + + self.v_explain = tk.Label(io, text='', font=('Consolas', 8), + bg=P['bg'], fg=P['muted'], + justify=tk.LEFT, wraplength=340) + self.v_explain.pack(anchor=tk.W, pady=(6, 0)) + + # Mapa char a char + map_f = tk.Frame(top, bg=P['bg']) + map_f.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + tk.Label(map_f, text='Mapeo carácter a carácter', + font=('Consolas', 9, 'bold'), + bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) + self.v_map_cv = tk.Canvas(map_f, height=100, bg=P['panel2'], + highlightthickness=0) + self.v_map_cv.pack(fill=tk.X, pady=(4, 0)) + self.v_map_cv.bind('', lambda e: self._draw_vig_map()) + + # Tabla Vigenère (scrollable) + tk.Label(parent, + text='Tabla de Vigenère (fila=clave, columna=plano, celda=cifrado)', + font=('Consolas', 8, 'bold'), bg=P['bg'], fg=P['muted'] + ).pack(padx=14, anchor=tk.W) + + tf = tk.Frame(parent, bg=P['bg']) + tf.pack(fill=tk.BOTH, expand=True, padx=14, pady=(2, 10)) + + self.v_tbl = tk.Canvas(tf, bg=P['panel2'], highlightthickness=0) + vb = ttk.Scrollbar(tf, orient=tk.VERTICAL, command=self.v_tbl.yview) + hb = ttk.Scrollbar(tf, orient=tk.HORIZONTAL, command=self.v_tbl.xview) + self.v_tbl.configure(yscrollcommand=vb.set, xscrollcommand=hb.set) + hb.pack(side=tk.BOTTOM, fill=tk.X) + vb.pack(side=tk.RIGHT, fill=tk.Y) + self.v_tbl.pack(fill=tk.BOTH, expand=True) + self._draw_vig_table() + + def _draw_vig_table(self, hr=-1, hc=-1): + cv = self.v_tbl + cv.delete('all') + CS = 20 + + for r in range(27): + for c in range(27): + x, y = c * CS, r * CS + if r == 0 and c == 0: + txt, bg, fg = '', P['panel'], P['muted'] + elif r == 0: + li = c - 1; hl = (li == hc) + txt = ALPHA[li] + bg = P['ac'] if hl else P['panel'] + fg = P['bg'] if hl else P['ac'] + elif c == 0: + li = r - 1; hl = (li == hr) + txt = ALPHA[li] + bg = P['bc'] if hl else P['panel'] + fg = P['bg'] if hl else P['bc'] + else: + letter = ALPHA[(r - 1 + c - 1) % N] + active = (r-1 == hr and c-1 == hc) + in_r = (r-1 == hr) + in_c = (c-1 == hc) + txt = letter + if active: + bg, fg = P['rc'], P['bg'] + elif in_r: + bg, fg = '#162210', P['rc'] + elif in_c: + bg, fg = '#101630', P['ac'] + else: + bg, fg = P['panel2'], P['muted'] + + cv.create_rectangle(x, y, x+CS, y+CS, + fill=bg, outline=P['bg'], width=1) + cv.create_text(x+CS//2, y+CS//2, text=txt, + font=('Consolas', 8, 'bold' + if r == 0 or c == 0 or (r-1==hr and c-1==hc) + else 'normal'), + fill=fg) + + total = 27 * CS + cv.configure(scrollregion=(0, 0, total, total)) + + def _update_vig(self, *_): + plain = ''.join(c for c in self.vig_plain.get().upper() if c in ALPHA) + key = ''.join(c for c in self.vig_key.get().upper() if c in ALPHA) + + self.v_out_e.config(state=tk.NORMAL) + if not plain or not key: + self.vig_out.set('') + self.v_out_e.config(state=tk.DISABLED) + self._draw_vig_table() + return + + cipher = [ALPHA[(ALPHA.index(p) + ALPHA.index(key[i % len(key)])) % N] + for i, p in enumerate(plain)] + + self.vig_out.set(''.join(cipher)) + self.v_out_e.config(state=tk.DISABLED) + + p0 = ALPHA.index(plain[0]); k0 = ALPHA.index(key[0]); c0 = (p0+k0) % N + self._draw_vig_table(hr=k0, hc=p0) + self.v_explain.config( + text=(f"'{plain[0]}' (col {p0}) + clave '{key[0]}' (fila {k0})\n" + f" → ({p0}+{k0}) mod 26 = {c0} → '{ALPHA[c0]}'")) + + self._vig_p = list(plain) + self._vig_k = list(key) + self._vig_c = cipher + self._draw_vig_map() + + def _draw_vig_map(self): + cv = self.v_map_cv + cv.delete('all') + pl = self._vig_p; ky = self._vig_k; ci = self._vig_c + if not pl: return + + W = cv.winfo_width() or 500 + n = min(len(pl), max(1, (W - 72) // 30)) + cell = max(26, (W - 72) // max(n, 1)) + + for ri, (letters, color, y, label) in enumerate([ + (pl[:n], P['ac'], 18, 'Plano '), + (ky[:n], P['bc'], 52, 'Clave '), + (ci[:n], P['rc'], 86, 'Cifrado'), + ]): + cv.create_text(6, y, text=label, anchor=tk.W, + font=('Consolas', 8), fill=P['muted']) + for j, letter in enumerate(letters): + x = 66 + j * cell + cell // 2 + if j == 0: + cv.create_rectangle(x-14, y-12, x+14, y+12, + fill=P['panel'], outline=color, width=2) + cv.create_text(x, y, text=letter, + font=('Consolas', 11, 'bold'), fill=color) + + # Marcadores de ciclo de clave + if label == 'Clave ': + kl = len(ky) + for j in range(1, n): + if j % kl == 0: + xm = 66 + j * cell + cv.create_line(xm, 34, xm, 70, fill=P['border'], dash=(3, 3)) + cv.create_text(xm, 32, text='↺', font=('Consolas', 7), + fill=P['bc']) + + # ══════════════════════════════════════════════════════════════════════════ + # TAB 3 · SUSTITUCIÓN + FRECUENCIAS + # ══════════════════════════════════════════════════════════════════════════ + def _build_substitution(self, parent): + top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10) + top.pack(fill=tk.X) + + left = tk.Frame(top, bg=P['bg']) + left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18)) + + # Modo + shuffle + mrow = tk.Frame(left, bg=P['bg']) + mrow.pack(fill=tk.X, pady=(0, 8)) + for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]: + tk.Radiobutton(mrow, text=txt, variable=self.sub_mode, value=val, + command=self._update_sub, + bg=P['bg'], fg=col, selectcolor=P['panel2'], + activebackground=P['bg'], + font=('Consolas', 10, 'bold') + ).pack(side=tk.LEFT, padx=(0, 18)) + + mk_btn(left, '🔀 Nueva clave aleatoria', P['btn'], + self._shuffle_sub).pack(fill=tk.X, pady=(0, 10)) + + for attr, lbl_txt, fg, editable in [ + ('s_in', 'Texto de entrada', P['ac'], True), + ('s_out', 'Resultado', P['rc'], False), + ]: + tk.Label(left, text=lbl_txt, font=('Consolas', 9), + bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) + w = tk.Text(left, height=4, width=36, + font=('Consolas', 11), wrap=tk.WORD, + bg=P['panel2'] if editable else P['panel'], + fg=fg, relief=tk.FLAT, insertbackground=fg, + state=tk.NORMAL if editable else tk.DISABLED) + w.pack(pady=(2, 8)) + setattr(self, attr, w) + self.s_in.bind('', lambda e: self._update_sub()) + self.s_in.insert('1.0', 'ESTAMOS APRENDIENDO CRIPTOGRAFIA') + + # Tabla de clave + right = tk.Frame(top, bg=P['bg']) + right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + tk.Label(right, text='Tabla de sustitución (A→? para cada letra)', + font=('Consolas', 9, 'bold'), + bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) + self.s_key_cv = tk.Canvas(right, height=72, bg=P['panel2'], + highlightthickness=0) + self.s_key_cv.pack(fill=tk.X, pady=(4, 0)) + self.s_key_cv.bind('', lambda e: self._draw_sub_key()) + + # Histograma de frecuencias + tk.Label(parent, + text='Análisis de Frecuencia (texto cifrado vs. español estándar)', + font=('Consolas', 9, 'bold'), bg=P['bg'], fg=P['muted'] + ).pack(padx=14, anchor=tk.W, pady=(6, 0)) + + self.s_freq_cv = tk.Canvas(parent, bg=P['panel2'], highlightthickness=0) + self.s_freq_cv.pack(fill=tk.BOTH, expand=True, padx=14, pady=(4, 10)) + self.s_freq_cv.bind('', lambda e: self._draw_freq()) + + self.root.after(120, self._update_sub) + + def _shuffle_sub(self): + random.shuffle(self.sub_key) + self._update_sub() + + def _update_sub(self, *_): + text = self.s_in.get('1.0', tk.END).strip().upper() + mode = self.sub_mode.get() + fwd = {ALPHA[i]: self.sub_key[i] for i in range(N)} + rev = {v: k for k, v in fwd.items()} + mapping = fwd if mode == 'E' else rev + + result = ''.join(mapping.get(ch, ch) if ch in ALPHA else ch for ch in text) + + self.s_out.config(state=tk.NORMAL) + self.s_out.delete('1.0', tk.END) + self.s_out.insert('1.0', result) + self.s_out.config(state=tk.DISABLED) + + self._sub_result = result + self._sub_mapping = mapping + self._draw_sub_key() + self._draw_freq() + + def _draw_sub_key(self): + cv = self.s_key_cv + cv.delete('all') + mapping = self._sub_mapping + if not mapping: return + W = cv.winfo_width() or 700 + cw = max(20, W // N) + + for i, letter in enumerate(ALPHA): + x = i * cw + cw // 2 + mapped = mapping.get(letter, letter) + cv.create_text(x, 14, text=letter, font=('Consolas', 9), fill=P['ac']) + cv.create_text(x, 36, text='↓', font=('Consolas', 8), fill=P['muted']) + cv.create_text(x, 58, text=mapped, font=('Consolas', 9, 'bold'), + fill=P['rc'] if mapped != letter else P['muted']) + + def _draw_freq(self): + cv = self.s_freq_cv + cv.delete('all') + W = cv.winfo_width() or 800 + H = cv.winfo_height() or 200 + if H < 60: return + + letters = [c for c in self._sub_result if c in ALPHA] + total = max(len(letters), 1) + ct_freq = {l: letters.count(l) / total * 100 for l in ALPHA} + + MAX_F = 16.0 + pad_l = 36 + pad_b = 28 + ch_w = W - pad_l - 10 + ch_h = H - pad_b - 20 + bot = H - pad_b + top_y = bot - ch_h + gw = ch_w // N + bw = max(3, gw // 2 - 1) + + # Eje Y + for pct in [0, 4, 8, 12, 16]: + yp = bot - int(pct / MAX_F * ch_h) + cv.create_line(pad_l - 4, yp, pad_l, yp, fill=P['muted']) + cv.create_text(pad_l - 6, yp, text=f'{pct}%', + anchor=tk.E, font=('Consolas', 7), fill=P['muted']) + + # Leyenda + cv.create_rectangle(pad_l, 4, pad_l + 10, 14, fill=P['bc'], outline='') + cv.create_text(pad_l + 14, 9, text='Español estándar', + anchor=tk.W, font=('Consolas', 7), fill=P['muted']) + cv.create_rectangle(pad_l + 130, 4, pad_l + 140, 14, fill=P['rc'], outline='') + cv.create_text(pad_l + 144, 9, text='Texto cifrado', + anchor=tk.W, font=('Consolas', 7), fill=P['muted']) + + for i, letter in enumerate(ALPHA): + xb = pad_l + i * gw + 2 + + # Barra español + es_h = int(min(FREQ_ES[letter], MAX_F) / MAX_F * ch_h) + cv.create_rectangle(xb, bot - es_h, xb + bw, bot, + fill=P['bc'], outline='') + + # Barra texto cifrado + ct_h = int(min(ct_freq[letter], MAX_F) / MAX_F * ch_h) + cv.create_rectangle(xb + bw + 1, bot - ct_h, + xb + bw * 2 + 1, bot, + fill=P['rc'], outline='') + + cv.create_text(xb + bw, bot + 10, text=letter, + font=('Consolas', 7), fill=P['muted']) + + +# ── Entry point ─────────────────────────────────────────────────────────────── +if __name__ == '__main__': + root = tk.Tk() + app = CifradosClasicos(root) + root.update_idletasks() + w, h = root.winfo_width(), root.winfo_height() + sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() + root.geometry(f'{w}x{h}+{(sw-w)//2}+{(sh-h)//2}') + root.mainloop() diff --git a/Lab1_XOR.py b/Lab1_XOR.py new file mode 100644 index 0000000..80baf8a --- /dev/null +++ b/Lab1_XOR.py @@ -0,0 +1,198 @@ +import tkinter as tk +from tkinter import filedialog, messagebox +from PIL import Image, ImageTk +import numpy as np +import os +import time + +class XORLab: + def __init__(self, root): + self.root = root + self.root.title("Lab 1: Fundamentos XOR - One Time Pad Visual") + self.root.geometry("1650x650") + + # Estado + self.img_original = None + self.img_key = None + self.img_cipher = None + + # UI Setup + frame_imgs = tk.Frame(root) + frame_imgs.pack(pady=10) + + # Paneles para imágenes con sus botones + # Panel 1: Cargar + self.panel_orig, self.lbl_entropy_orig = self.create_panel(frame_imgs, "1. Mensaje (Imagen)", + buttons=[("Cargar Imagen", self.load_image)]) + + # Panel 2: Generar Clave + self.panel_key, self.lbl_entropy_key = self.create_panel(frame_imgs, "2. Clave (Ruido)", + buttons=[("Generar Clave (Ruido)", self.generate_key)]) + + # Panel 3: Cifrar (Normal y Lento) + self.panel_cipher, self.lbl_entropy_cipher = self.create_panel(frame_imgs, "3. Resultado (Cifrado)", + buttons=[("Cifrar (XOR)", self.encrypt), + ("Cifrado Lento (Visual)", self.encrypt_slow)]) + + # Panel 4: Descifrar (Normal y Lento) + self.panel_decrypted, self.lbl_entropy_decrypted = self.create_panel(frame_imgs, "4. Descifrado (Original)", + buttons=[("Descifrar (XOR Inverso)", self.decrypt), + ("Descifrado Lento (Visual)", self.decrypt_slow)]) + + def create_panel(self, parent, title, buttons=[]): + frame = tk.Frame(parent) + frame.pack(side=tk.LEFT, padx=15) + tk.Label(frame, text=title, font=("Arial", 11, "bold")).pack() + canvas = tk.Canvas(frame, width=350, height=350, bg="#ddd") + canvas.pack() + lbl_entropy = tk.Label(frame, text="Entropía: -", font=("Arial", 10), fg="blue") + lbl_entropy.pack(pady=5) + + for btn_text, btn_command in buttons: + tk.Button(frame, text=btn_text, command=btn_command, font=("Arial", 10)).pack(pady=2, fill=tk.X) + + return canvas, lbl_entropy + + def load_image(self): + path = filedialog.askopenfilename(initialdir=os.getcwd(), filetypes=[("Images", ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.PNG", "*.JPG", "*.JPEG", "*.BMP")), ("All Files", "*.*")]) + if path: + img = Image.open(path).resize((350, 350)).convert('RGB') + self.img_original = np.array(img) + self.show_image(self.panel_orig, self.lbl_entropy_orig, self.img_original) + # Limpiar otros paneles + self.panel_key.delete("all") + self.lbl_entropy_key.config(text="Entropía: -") + self.panel_cipher.delete("all") + self.lbl_entropy_cipher.config(text="Entropía: -") + self.panel_decrypted.delete("all") + self.lbl_entropy_decrypted.config(text="Entropía: -") + + def generate_key(self): + if self.img_original is None: + messagebox.showwarning("Error", "Primero cargue una imagen.") + return + # Generar clave aleatoria del mismo tamaño (One Time Pad) + self.img_key = np.random.randint(0, 256, self.img_original.shape, dtype=np.uint8) + self.show_image(self.panel_key, self.lbl_entropy_key, self.img_key) + # Limpiar resultado + self.panel_cipher.delete("all") + self.lbl_entropy_cipher.config(text="Entropía: -") + self.panel_decrypted.delete("all") + self.lbl_entropy_decrypted.config(text="Entropía: -") + + def encrypt(self): + if self.img_original is None: + messagebox.showwarning("Error", "Primero cargue una imagen.") + return + if self.img_key is None: + messagebox.showwarning("Error", "Primero genere una clave.") + return + + # Operación XOR + self.img_cipher = np.bitwise_xor(self.img_original, self.img_key) + self.show_image(self.panel_cipher, self.lbl_entropy_cipher, self.img_cipher) + + def encrypt_slow(self): + if self.img_original is None: + messagebox.showwarning("Error", "Primero cargue una imagen.") + return + if self.img_key is None: + messagebox.showwarning("Error", "Primero genere una clave.") + return + + # 1. Cargar imagen original en el cuadro de resultado + working_img = np.copy(self.img_original) + self.img_cipher = np.copy(working_img) # Inicializar + self.show_image(self.panel_cipher, self.lbl_entropy_cipher, working_img) + self.root.update() + time.sleep(1) + + # 2. Aplicar XOR con delay row-by-row + rows, cols, channels = working_img.shape + for r in range(rows): + # XOR de la fila r + row_key = self.img_key[r, :, :] + row_orig = self.img_original[r, :, :] + xor_row = np.bitwise_xor(row_orig, row_key) + + # Actualizar imagen de trabajo + working_img[r, :, :] = xor_row + + + # delay cada 5 filas para balancear velocidad + if r % 5 == 0: + self.show_image(self.panel_cipher, self.lbl_entropy_cipher, working_img) + self.root.update() + # Pequeño delay + time.sleep(0.1) + + # Final update + self.img_cipher = working_img + self.show_image(self.panel_cipher, self.lbl_entropy_cipher, self.img_cipher) + + def decrypt(self): + if self.img_cipher is None or self.img_key is None: return + # XOR de vuelta + decrypted = np.bitwise_xor(self.img_cipher, self.img_key) + # Mostrar cuarto panel + self.show_image(self.panel_decrypted, self.lbl_entropy_decrypted, decrypted) + + def decrypt_slow(self): + if self.img_cipher is None: + messagebox.showwarning("Error", "Primero debe cifrar una imagen.") + return + if self.img_key is None: + messagebox.showwarning("Error", "Falta la clave.") + return + + # 1. Cargar el resultado (cifrado) en el cuadro descifrado + working_img = np.copy(self.img_cipher) + self.show_image(self.panel_decrypted, self.lbl_entropy_decrypted, working_img) + self.root.update() + time.sleep(1) + + # 2. Aplicar XOR inverso (que es igual a XOR) row-by-row + rows, cols, channels = working_img.shape + for r in range(rows): + # XOR de la fila r + row_key = self.img_key[r, :, :] + row_cipher = self.img_cipher[r, :, :] + xor_row = np.bitwise_xor(row_cipher, row_key) + + # Actualizar imagen de trabajo + working_img[r, :, :] = xor_row + + if r % 5 == 0: + self.show_image(self.panel_decrypted, self.lbl_entropy_decrypted, working_img) + self.root.update() + time.sleep(0.1) + + self.show_image(self.panel_decrypted, self.lbl_entropy_decrypted, working_img) + + def calculate_entropy(self, img_array): + # Aplanar array para considerar todos los canales/pixeles + flat_img = img_array.flatten() + # Calcular histograma + histogram, _ = np.histogram(flat_img, bins=256, range=(0, 256)) + # Probabilidades + prob = histogram / float(flat_img.size) + # Filtrar ceros para log + prob = prob[prob > 0] + # Shannon + entropy = -np.sum(prob * np.log2(prob)) + return entropy + + def show_image(self, panel, lbl_entropy, img_array): + img = Image.fromarray(img_array) + img_tk = ImageTk.PhotoImage(img) + panel.image = img_tk # Mantener referencia + panel.create_image(175, 175, image=img_tk) + + # Calcular y mostrar entropía + ent = self.calculate_entropy(img_array) + lbl_entropy.config(text=f"Entropía: {ent:.4f} bits") + +if __name__ == "__main__": + root = tk.Tk() + app = XORLab(root) + root.mainloop() \ No newline at end of file diff --git a/Lab2_Hashing.py b/Lab2_Hashing.py new file mode 100644 index 0000000..93eaf10 --- /dev/null +++ b/Lab2_Hashing.py @@ -0,0 +1,88 @@ +import tkinter as tk +import hashlib +import difflib + +class HashLab: + def __init__(self, root): + self.root = root + self.root.title("Lab 2: Efecto Avalancha (SHA-256)") + self.root.geometry("800x450") + + # Inputs + tk.Label(root, text="Entrada A:").pack(anchor="w", padx=20) + self.input_a = tk.Entry(root, width=80, font=("Consolas", 10)) + self.input_a.pack(padx=20, pady=5) + self.input_a.bind("", self.update) + + tk.Label(root, text="Entrada B:").pack(anchor="w", padx=20) + self.input_b = tk.Entry(root, width=80, font=("Consolas", 10)) + self.input_b.pack(padx=20, pady=5) + self.input_b.bind("", self.update) + + # Diferencia Textual + self.lbl_text_diff = tk.Label(root, text="Diferencia de texto: 0.00%", font=("Arial", 10, "italic"), fg="#555") + self.lbl_text_diff.pack(pady=5) + + # Visualización Hashes + tk.Label(root, text="Hash A (SHA-256):").pack(anchor="w", padx=20, pady=(20,0)) + self.lbl_hash_a = tk.Label(root, text="", font=("Consolas", 9), fg="blue", wraplength=750) + self.lbl_hash_a.pack(padx=20) + + tk.Label(root, text="Hash B (SHA-256):").pack(anchor="w", padx=20) + self.lbl_hash_b = tk.Label(root, text="", font=("Consolas", 9), fg="blue", wraplength=750) + self.lbl_hash_b.pack(padx=20) + + # Estadísticas + self.lbl_stats = tk.Label(root, text="Diferencia de bits: 0 / 256 (0.00%)", font=("Arial", 12, "bold")) + self.lbl_stats.pack(pady=30) + + # Canvas para bits + self.canvas_bits = tk.Canvas(root, width=768, height=20, bg="white") + self.canvas_bits.pack(pady=10) + + def update(self, event=None): + text_str_a = self.input_a.get() + text_str_b = self.input_b.get() + + # Calcular diferencia textual + matcher = difflib.SequenceMatcher(None, text_str_a, text_str_b) + ratio = matcher.ratio() # 1.0 es igual, 0.0 es diferente + diff_pct = (1.0 - ratio) * 100 + self.lbl_text_diff.config(text=f"Diferencia de texto: {diff_pct:.2f}%") + + text_a = text_str_a.encode() + text_b = text_str_b.encode() + + hash_a_hex = hashlib.sha256(text_a).hexdigest() + hash_b_hex = hashlib.sha256(text_b).hexdigest() + + self.lbl_hash_a.config(text=hash_a_hex) + self.lbl_hash_b.config(text=hash_b_hex) + + # Convertir a binario + bin_a = bin(int(hash_a_hex, 16))[2:].zfill(256) + bin_b = bin(int(hash_b_hex, 16))[2:].zfill(256) + + # Comparar bits + diffs = 0 + self.canvas_bits.delete("all") + w = 3 # ancho por bit visual + for i in range(256): + color = "#eee" # igual + if bin_a[i] != bin_b[i]: + diffs += 1 + color = "red" # Diferente + self.canvas_bits.create_rectangle(i*w, 0, (i*w)+w, 20, fill=color, outline="") + + pct = (diffs / 256) * 100 + self.lbl_stats.config(text=f"Diferencia de bits: {diffs} / 256 ({pct:.2f}%)") + + # Color semántico + if diffs == 0: self.lbl_stats.config(fg="green") + elif pct < 40: self.lbl_stats.config(fg="orange") # Avalancha pobre (raro en sha256) + else: self.lbl_stats.config(fg="red") # gran avalancha del hash + +if __name__ == "__main__": + root = tk.Tk() + app = HashLab(root) + root.mainloop() \ No newline at end of file diff --git a/Lab3_ECB_CBC_v2.py b/Lab3_ECB_CBC_v2.py new file mode 100644 index 0000000..be723fa --- /dev/null +++ b/Lab3_ECB_CBC_v2.py @@ -0,0 +1,515 @@ +""" +Lab 3: Modos de operacion AES - herramienta pedagogica + +Pestana 1 ECB vs CBC ........ patron visual, entropia de Shannon e histograma +Pestana 2 Maleabilidad ...... bit-flipping sobre CBC, y por que GCM lo detiene +Pestana 3 Reuso de nonce .... dos imagenes con el mismo nonce en CTR + +Ninguna de estas construcciones debe usarse en produccion: claves fijas, +IV predecibles y ausencia de autenticacion son parte de la demostracion. +""" + +import math +import tkinter as tk +from collections import Counter +from tkinter import filedialog, messagebox, ttk + +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from PIL import Image, ImageTk + +# --- Parametros ajustables ------------------------------------------------- +IMG_SIZE = 400 # lado de las imagenes de la pestana 1 (px) +SMALL_SIZE = 280 # lado de las imagenes de la pestana 3 (px) +HIST_H = 55 # alto del histograma (px) +BLOCK = AES.block_size # 16 bytes +STEP_BYTES = IMG_SIZE * 3 * 2 # avance por frame de animacion (2 filas RGB) +FRAME_DELAY = 10 # ms entre frames + +KEY = b'Sixteen byte key' # clave ASCII de baja entropia: SOLO demostracion +IV_FIJO = b'Sixteen byte iv.' + + +# --- Metricas y utilidades ------------------------------------------------- +def shannon_entropy(data: bytes) -> float: + """Entropia de Shannon en bits/byte. Maximo teorico: 8.0""" + if not data: + return 0.0 + n = len(data) + return -sum((c / n) * math.log2(c / n) for c in Counter(data).values()) + + +def block_stats(data: bytes): + """Total, unicos y repetidos de bloques de 16 bytes.""" + usable = len(data) - (len(data) % BLOCK) + blocks = [data[i:i + BLOCK] for i in range(0, usable, BLOCK)] + total = len(blocks) + unique = len(set(blocks)) + return total, unique, total - unique + + +def format_metrics(data: bytes) -> str: + h = shannon_entropy(data) + total, unique, repeated = block_stats(data) + pct = (repeated / total * 100) if total else 0.0 + return (f"Entropia Shannon: {h:.4f} / 8.0 bits/byte\n" + f"Bloques 16B: {total:,} | unicos: {unique:,}\n" + f"Repetidos: {repeated:,} ({pct:.1f}%)") + + +def xor_bytes(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + + +def printable(data: bytes) -> str: + """Bytes no imprimibles se muestran como punto medio.""" + return ''.join(chr(c) if 32 <= c < 127 else '.' for c in data) + + +def draw_histogram(canvas, data, color="#2c6fbb"): + """Histograma de los 256 valores de byte, dibujado en un Canvas.""" + canvas.delete("all") + w = int(canvas.cget("width")) + h = int(canvas.cget("height")) + if not data: + return + counts = Counter(data) + max_c = max(counts.values()) + bar_w = w / 256.0 + for b in range(256): + c = counts.get(b, 0) + if not c: + continue + bh = (c / max_c) * (h - 2) + x0 = b * bar_w + canvas.create_rectangle(x0, h - bh, x0 + bar_w, h, + fill=color, outline="") + canvas.create_line(0, h - 1, w, h - 1, fill="#888") + + +# =========================================================================== +class AESLab: + def __init__(self, root): + self.root = root + self.root.title("Lab 3: Modos de Operacion AES") + self.root.geometry("1380x800") + + nb = ttk.Notebook(root) + nb.pack(fill="both", expand=True, padx=8, pady=6) + + self.tab_modes = tk.Frame(nb) + self.tab_flip = tk.Frame(nb) + self.tab_ctr = tk.Frame(nb) + nb.add(self.tab_modes, text=" 1. ECB vs CBC ") + nb.add(self.tab_flip, text=" 2. Maleabilidad de CBC ") + nb.add(self.tab_ctr, text=" 3. Reuso de nonce (CTR) ") + + self.build_tab_modes() + self.build_tab_flip() + self.build_tab_ctr() + + # ======================================================================= + # PESTANA 1: ECB vs CBC + # ======================================================================= + def build_tab_modes(self): + t = self.tab_modes + self.img_bytes_raw = None + self.original_img = None + self.busy = False + self.random_iv = tk.BooleanVar(value=False) + + panels = tk.Frame(t) + panels.pack(pady=8) + self.p_orig = self.make_panel(panels, "Original (plaintext)", IMG_SIZE) + self.p_ecb = self.make_panel(panels, "ECB - INSEGURO", IMG_SIZE) + self.p_cbc = self.make_panel(panels, "CBC - sin patron visible", IMG_SIZE) + + ctrl = tk.Frame(t) + ctrl.pack(pady=6) + self.btn_load = tk.Button(ctrl, text="1. Cargar Imagen", + command=self.load_image) + self.btn_load.pack(side=tk.LEFT, padx=8) + self.btn_ecb = tk.Button(ctrl, text="2. Ejecutar ECB", + command=self.run_ecb) + self.btn_ecb.pack(side=tk.LEFT, padx=8) + self.btn_cbc = tk.Button(ctrl, text="3. Ejecutar CBC", + command=self.run_cbc) + self.btn_cbc.pack(side=tk.LEFT, padx=8) + tk.Checkbutton(ctrl, text="IV aleatorio por ejecucion", + variable=self.random_iv).pack(side=tk.LEFT, padx=15) + + info = tk.Frame(t) + info.pack(fill="x", padx=20, pady=4) + tk.Label(info, font=("Courier", 9), anchor="w", + text=f"Llave (Hex): {KEY.hex().upper()} " + f"[ASCII de baja entropia: solo demostracion]" + ).pack(anchor="w") + self.lbl_status = tk.Label(info, text="Estado: esperando imagen...", + font=("Arial", 10, "bold"), fg="blue", + anchor="w") + self.lbl_status.pack(anchor="w") + tk.Label(info, justify="left", fg="#444", font=("Arial", 9), anchor="w", + text=("La entropia y el histograma de ECB y CBC son practicamente " + "identicos: NO sirven para detectar el modo inseguro.\n" + "El indicador valido es 'Repetidos': ECB conserva los bloques " + "duplicados del original, CBC los elimina por encadenamiento.") + ).pack(anchor="w") + + def make_panel(self, parent, title, size, with_hist=True): + f = tk.Frame(parent) + f.pack(side=tk.LEFT, padx=10) + tk.Label(f, text=title, font=("Arial", 10, "bold")).pack() + + # Contenedor de tamano fijo EN PIXELES: un Label sin imagen mide su + # width en caracteres, no en pixeles. + holder = tk.Frame(f, width=size, height=size, bg="#ccc") + holder.pack() + holder.pack_propagate(False) + img_lbl = tk.Label(holder, bg="#ccc", borderwidth=0) + img_lbl.pack(fill="both", expand=True) + + stats_lbl = tk.Label(f, text="Entropia: -\nBloques: -\nRepetidos: -", + font=("Courier", 9), justify="left", + anchor="w", width=42) + stats_lbl.pack(pady=(4, 0)) + + hist = None + if with_hist: + hist = tk.Canvas(f, width=size, height=HIST_H, bg="#f7f7f7", + highlightthickness=1, highlightbackground="#ddd") + hist.pack() + tk.Label(f, text="histograma de bytes 0 ... 255", + font=("Arial", 7), fg="#777").pack() + + return {"img": img_lbl, "stats": stats_lbl, "hist": hist} + + def update_panel_metrics(self, panel, data, color="#2c6fbb"): + panel["stats"].config(text=format_metrics(data)) + if panel["hist"] is not None: + draw_histogram(panel["hist"], data, color) + + def clear_panel(self, panel): + panel["img"].config(image='', bg="#ccc") + panel["img"].image = None + panel["stats"].config(text="Entropia: -\nBloques: -\nRepetidos: -") + if panel["hist"] is not None: + panel["hist"].delete("all") + + def set_busy(self, busy): + self.busy = busy + state = tk.DISABLED if busy else tk.NORMAL + for b in (self.btn_load, self.btn_ecb, self.btn_cbc): + b.config(state=state) + + def load_image(self): + path = filedialog.askopenfilename( + initialdir=".", + filetypes=[("Images", ("*.png", "*.jpg", "*.jpeg", "*.bmp")), + ("All Files", "*.*")]) + if not path: + return + try: + img = (Image.open(path) + .resize((IMG_SIZE, IMG_SIZE), Image.LANCZOS) + .convert('RGB')) + self.img_bytes_raw = img.tobytes() + self.original_img = img + + self.show(self.p_orig, img) + self.update_panel_metrics(self.p_orig, self.img_bytes_raw, "#4a7c3f") + self.clear_panel(self.p_ecb) + self.clear_panel(self.p_cbc) + + total = len(self.img_bytes_raw) // BLOCK + self.lbl_status.config( + text=f"Imagen cargada. Bytes: {len(self.img_bytes_raw):,} | " + f"Bloques de 16B: {total:,}") + except Exception as e: + messagebox.showerror("Error", f"No se pudo cargar la imagen: {e}") + + def check_ready(self): + if self.busy: + return False + if self.img_bytes_raw is None: + messagebox.showwarning("Aviso", "Primero carga una imagen.") + return False + return True + + def run_ecb(self): + if not self.check_ready(): + return + ct = AES.new(KEY, AES.MODE_ECB).encrypt(pad(self.img_bytes_raw, BLOCK)) + self.lbl_status.config(text="Iniciando cifrado ECB...") + self.set_busy(True) + self.animate(self.p_ecb, ct[:len(self.img_bytes_raw)], + self.img_bytes_raw, mode_name="ECB", color="#b03030") + + def run_cbc(self): + if not self.check_ready(): + return + iv = get_random_bytes(BLOCK) if self.random_iv.get() else IV_FIJO + ct = AES.new(KEY, AES.MODE_CBC, iv).encrypt(pad(self.img_bytes_raw, BLOCK)) + self.lbl_status.config( + text=f"Iniciando cifrado CBC... IV = {iv.hex().upper()}") + self.set_busy(True) + self.animate(self.p_cbc, ct[:len(self.img_bytes_raw)], + self.img_bytes_raw, mode_name="CBC", color="#2c6fbb") + + def animate(self, panel, target, original, current_idx=0, + mode_name="", color="#2c6fbb"): + if current_idx >= len(original): + self.show_bytes(panel, target) + self.update_panel_metrics(panel, target, color) + self.lbl_status.config(text=f"Cifrado {mode_name} completado.") + self.set_busy(False) + return + + self.show_bytes(panel, target[:current_idx] + original[current_idx:]) + self.lbl_status.config( + text=f"Cifrando {mode_name}... bloque {current_idx // BLOCK:,}" + f" / {len(original) // BLOCK:,}") + self.root.after(FRAME_DELAY, + lambda: self.animate(panel, target, original, + current_idx + STEP_BYTES, + mode_name, color)) + + def show(self, panel, img_obj): + tk_img = ImageTk.PhotoImage(img_obj) + panel["img"].config(image=tk_img) + panel["img"].image = tk_img + + def show_bytes(self, panel, data, size=IMG_SIZE): + try: + self.show(panel, Image.frombytes('RGB', (size, size), data)) + except Exception as e: + print(f"Error mostrando bytes: {e}") + + # ======================================================================= + # PESTANA 2: maleabilidad de CBC (bit-flipping) + # ======================================================================= + def build_tab_flip(self): + t = self.tab_flip + self.flip_iv = None + self.flip_ct = None + self.flip_blocks = ["usuario=esteban;", "rol=invitado;;;;", + "saldo=0000001000"] + + tk.Label(t, font=("Arial", 10), justify="left", anchor="w", + text=("CBC entrega confidencialidad, NO integridad. Un atacante que no conoce la clave " + "puede alterar el bloque de texto claro N modificando el bloque de cifrado N-1.\n" + "El precio es que el bloque N-1 queda destruido; si ese bloque es relleno o poco " + "revisado, el ataque pasa desapercibido.") + ).pack(anchor="w", padx=20, pady=(12, 8)) + + pt_frame = tk.LabelFrame(t, text="Mensaje original (3 bloques de 16 bytes, sin relleno)", + padx=10, pady=8) + pt_frame.pack(fill="x", padx=20) + for i, b in enumerate(self.flip_blocks): + tk.Label(pt_frame, font=("Courier", 11), anchor="w", + text=f" Bloque {i}: |{b}|").pack(anchor="w") + + atk = tk.LabelFrame(t, text="Ataque", padx=10, pady=8) + atk.pack(fill="x", padx=20, pady=10) + tk.Label(atk, text="El atacante quiere que el bloque 1 diga " + "(exactamente 16 caracteres):", + font=("Arial", 10)).pack(anchor="w") + self.flip_target = tk.Entry(atk, font=("Courier", 11), width=24) + self.flip_target.insert(0, "rol=admin;;;;;;;") + self.flip_target.pack(anchor="w", pady=4) + + btns = tk.Frame(atk) + btns.pack(anchor="w", pady=4) + tk.Button(btns, text="1. Cifrar con CBC", + command=self.flip_encrypt).pack(side=tk.LEFT, padx=6) + tk.Button(btns, text="2. Manipular el ciphertext", + command=self.flip_attack).pack(side=tk.LEFT, padx=6) + tk.Button(btns, text="3. Repetir el ataque contra AES-GCM", + command=self.flip_gcm).pack(side=tk.LEFT, padx=6) + + self.flip_out = tk.Text(t, height=16, font=("Courier", 10), + bg="#1e1e1e", fg="#dcdcdc", + insertbackground="#dcdcdc", wrap="none") + self.flip_out.pack(fill="both", expand=True, padx=20, pady=(0, 14)) + self.flip_log("Pulsa '1. Cifrar con CBC' para comenzar.") + + def flip_log(self, text, clear=False): + if clear: + self.flip_out.delete("1.0", tk.END) + self.flip_out.insert(tk.END, text + "\n") + self.flip_out.see(tk.END) + + def flip_plaintext(self) -> bytes: + return "".join(self.flip_blocks).encode() + + def flip_encrypt(self): + self.flip_iv = get_random_bytes(BLOCK) + self.flip_ct = AES.new(KEY, AES.MODE_CBC, + self.flip_iv).encrypt(self.flip_plaintext()) + self.flip_log("=== CIFRADO LEGITIMO (CBC) ===", clear=True) + self.flip_log(f"IV : {self.flip_iv.hex().upper()}") + for i in range(3): + c = self.flip_ct[i * BLOCK:(i + 1) * BLOCK] + self.flip_log(f"C{i} : {c.hex().upper()}") + self.flip_log("") + self.flip_log("El atacante intercepta el ciphertext. No conoce la clave.") + + def flip_attack(self): + if self.flip_ct is None: + messagebox.showwarning("Aviso", "Primero cifra el mensaje (paso 1).") + return + target = self.flip_target.get().encode() + if len(target) != BLOCK: + messagebox.showerror( + "Error", + f"El texto objetivo debe medir exactamente {BLOCK} caracteres " + f"(actualmente mide {len(target)}).") + return + + # P1' = D(C1) xor C0' => C0' = C0 xor P1 xor P1' + delta = xor_bytes(self.flip_blocks[1].encode(), target) + forged = bytearray(self.flip_ct) + for j in range(BLOCK): + forged[j] ^= delta[j] + forged = bytes(forged) + + recovered = AES.new(KEY, AES.MODE_CBC, + self.flip_iv).decrypt(forged) + + self.flip_log("") + self.flip_log("=== ATAQUE DE BIT-FLIPPING ===") + self.flip_log(f"Delta aplicado a C0 : {delta.hex().upper()}") + self.flip_log(f"C0 manipulado : {forged[:BLOCK].hex().upper()}") + self.flip_log("") + self.flip_log("Descifrado en el servidor (la clave nunca se toco):") + for i in range(3): + b = recovered[i * BLOCK:(i + 1) * BLOCK] + mark = " <-- DESTRUIDO" if i == 0 else (" <-- CONTROLADO POR EL ATACANTE" + if i == 1 else "") + self.flip_log(f" Bloque {i}: |{printable(b)}|{mark}") + self.flip_log("") + self.flip_log("El atacante escribio el bloque 1 a voluntad sin conocer la clave.") + self.flip_log("CBC sin MAC no detecta nada: descifra y entrega el resultado.") + + def flip_gcm(self): + pt = self.flip_plaintext() + nonce = get_random_bytes(12) + ct, tag = AES.new(KEY, AES.MODE_GCM, nonce=nonce).encrypt_and_digest(pt) + + target = self.flip_target.get().encode() + if len(target) != BLOCK: + target = b"rol=admin;;;;;;;" + # En CTR/GCM el flip es aun mas directo: se altera el bloque objetivo. + delta = xor_bytes(self.flip_blocks[1].encode(), target) + forged = bytearray(ct) + for j in range(BLOCK): + forged[BLOCK + j] ^= delta[j] + + self.flip_log("") + self.flip_log("=== MISMO ATAQUE CONTRA AES-GCM ===") + self.flip_log(f"Nonce: {nonce.hex().upper()}") + self.flip_log(f"Tag : {tag.hex().upper()}") + try: + rec = AES.new(KEY, AES.MODE_GCM, + nonce=nonce).decrypt_and_verify(bytes(forged), tag) + self.flip_log(f"Descifrado: {printable(rec)}") + except ValueError: + self.flip_log("") + self.flip_log(" MAC INVALIDO -> el mensaje se rechaza por completo.") + self.flip_log(" GCM verifica la integridad ANTES de entregar texto claro.") + self.flip_log("") + self.flip_log("Conclusion: el cifrado autenticado (AEAD) no es un extra,") + self.flip_log("es el requisito minimo para cualquier canal real.") + + # ======================================================================= + # PESTANA 3: reuso de nonce en CTR + # ======================================================================= + def build_tab_ctr(self): + t = self.tab_ctr + self.ctr_a = None + self.ctr_b = None + + tk.Label(t, font=("Arial", 10), justify="left", anchor="w", + text=("CTR convierte AES en un cifrado de flujo: C = P xor keystream. " + "Si dos mensajes usan el mismo nonce comparten keystream, y entonces\n" + " C_A xor C_B = P_A xor P_B ... el keystream se cancela y el " + "atacante ve la relacion entre ambos textos claros.") + ).pack(anchor="w", padx=20, pady=(12, 8)) + + panels = tk.Frame(t) + panels.pack(pady=4) + self.p_a = self.make_panel(panels, "Imagen A", SMALL_SIZE, with_hist=False) + self.p_b = self.make_panel(panels, "Imagen B", SMALL_SIZE, with_hist=False) + self.p_xor_same = self.make_panel(panels, "C_A xor C_B (MISMO nonce)", + SMALL_SIZE, with_hist=False) + self.p_xor_diff = self.make_panel(panels, "C_A xor C_B (nonces distintos)", + SMALL_SIZE, with_hist=False) + + ctrl = tk.Frame(t) + ctrl.pack(pady=8) + tk.Button(ctrl, text="Cargar Imagen A", + command=lambda: self.ctr_load('a')).pack(side=tk.LEFT, padx=8) + tk.Button(ctrl, text="Cargar Imagen B", + command=lambda: self.ctr_load('b')).pack(side=tk.LEFT, padx=8) + tk.Button(ctrl, text="Cifrar y comparar", + command=self.ctr_run).pack(side=tk.LEFT, padx=8) + + self.ctr_status = tk.Label(t, text="Carga dos imagenes distintas.", + font=("Arial", 10, "bold"), fg="blue") + self.ctr_status.pack(pady=4) + + def ctr_load(self, which): + path = filedialog.askopenfilename( + initialdir=".", + filetypes=[("Images", ("*.png", "*.jpg", "*.jpeg", "*.bmp")), + ("All Files", "*.*")]) + if not path: + return + try: + img = (Image.open(path) + .resize((SMALL_SIZE, SMALL_SIZE), Image.LANCZOS) + .convert('RGB')) + data = img.tobytes() + if which == 'a': + self.ctr_a = data + self.show(self.p_a, img) + self.p_a["stats"].config(text=format_metrics(data)) + else: + self.ctr_b = data + self.show(self.p_b, img) + self.p_b["stats"].config(text=format_metrics(data)) + self.ctr_status.config(text="Imagen cargada.") + except Exception as e: + messagebox.showerror("Error", f"No se pudo cargar la imagen: {e}") + + def ctr_run(self): + if self.ctr_a is None or self.ctr_b is None: + messagebox.showwarning("Aviso", "Carga las dos imagenes primero.") + return + + nonce = get_random_bytes(8) + ca = AES.new(KEY, AES.MODE_CTR, nonce=nonce).encrypt(self.ctr_a) + cb = AES.new(KEY, AES.MODE_CTR, nonce=nonce).encrypt(self.ctr_b) + xor_same = xor_bytes(ca, cb) + + c2a = AES.new(KEY, AES.MODE_CTR, + nonce=get_random_bytes(8)).encrypt(self.ctr_a) + c2b = AES.new(KEY, AES.MODE_CTR, + nonce=get_random_bytes(8)).encrypt(self.ctr_b) + xor_diff = xor_bytes(c2a, c2b) + + self.show_bytes(self.p_xor_same, xor_same, SMALL_SIZE) + self.p_xor_same["stats"].config(text=format_metrics(xor_same)) + self.show_bytes(self.p_xor_diff, xor_diff, SMALL_SIZE) + self.p_xor_diff["stats"].config(text=format_metrics(xor_diff)) + + self.ctr_status.config( + text=f"Nonce reutilizado: {nonce.hex().upper()} -> el XOR de los " + f"ciphertexts es exactamente P_A xor P_B (ambas siluetas visibles). " + f"Con nonces distintos, solo ruido.") + + +if __name__ == "__main__": + root = tk.Tk() + app = AESLab(root) + root.mainloop() diff --git a/XOR_visualizer.py b/XOR_visualizer.py new file mode 100644 index 0000000..3bd24c1 --- /dev/null +++ b/XOR_visualizer.py @@ -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()