feat: add cryptography fundamentals lab scripts
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.atl/
|
||||
__pycache__/
|
||||
599
Lab0_cifrados_clasicos.py
Normal file
599
Lab0_cifrados_clasicos.py
Normal 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
198
Lab1_XOR.py
Normal 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
88
Lab2_Hashing.py
Normal 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
515
Lab3_ECB_CBC_v2.py
Normal 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
511
XOR_visualizer.py
Normal 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()
|
||||
Reference in New Issue
Block a user