feat: add cryptography fundamentals lab scripts
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user