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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.atl/
__pycache__/

599
Lab0_cifrados_clasicos.py Normal file
View File

@@ -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('<KeyRelease>', 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('<Configure>', 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('<KeyRelease>', 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('<Configure>', 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('<Configure>', 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()

198
Lab1_XOR.py Normal file
View File

@@ -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()

88
Lab2_Hashing.py Normal file
View File

@@ -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("<KeyRelease>", 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("<KeyRelease>", 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()

515
Lab3_ECB_CBC_v2.py Normal file
View File

@@ -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()

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