#!/usr/bin/env python3 """ cifrados_clasicos.py — Tres Cifrados Clásicos en Tabs Herramienta pedagógica · Fundamentos de Criptografía Requiere Python 3.x (tkinter incluido en stdlib) Ejecutar: python cifrados_clasicos.py """ import tkinter as tk from tkinter import ttk import string, math, random ALPHA = string.ascii_uppercase N = 26 # Frecuencias del español (%) FREQ_ES = { 'A':12.53,'B':1.42,'C':4.68,'D':5.86,'E':13.68,'F':0.69,'G':1.01, 'H':0.70,'I':6.25,'J':0.44,'K':0.02,'L':4.97,'M':3.15,'N':6.71, 'O':8.68,'P':2.51,'Q':0.88,'R':6.87,'S':7.98,'T':4.63,'U':3.93, 'V':0.90,'W':0.01,'X':0.22,'Y':0.90,'Z':0.52, } P = dict( bg='#0d0d1a', panel='#13132b', panel2='#1a1a35', border='#2a2a50', text='#dde1f0', muted='#555580', accent='#818cf8', ac='#38bdf8', bc='#fb923c', rc='#4ade80', btn='#4f46e5', hl='#fde68a', ) def mk_btn(parent, label, color, cmd, **kw): return tk.Button(parent, text=label, font=('Consolas', 9, 'bold'), bg=color, fg='white', relief=tk.FLAT, padx=10, pady=5, cursor='hand2', command=cmd, **kw) class CifradosClasicos: def __init__(self, root): self.root = root root.title('Cifrados Clásicos — Fundamentos de Criptografía') root.configure(bg=P['bg']) root.geometry('1080x760') root.minsize(860, 620) self.shift_var = tk.IntVar(value=3) self.cmode_var = tk.StringVar(value='E') self.vig_plain = tk.StringVar() self.vig_key = tk.StringVar(value='CLAVE') self.vig_out = tk.StringVar() self.sub_mode = tk.StringVar(value='E') self.sub_key = list(ALPHA) random.shuffle(self.sub_key) # caches self._vig_p = self._vig_k = self._vig_c = [] self._sub_result = '' self._sub_mapping = {} self._build() # ── Scaffold ────────────────────────────────────────────────────────────── def _build(self): bar = tk.Frame(self.root, bg=P['panel'], pady=10) bar.pack(fill=tk.X) tk.Label(bar, text='🔐 Cifrados Clásicos', font=('Consolas', 15, 'bold'), bg=P['panel'], fg=P['accent']).pack(side=tk.LEFT, padx=18) tk.Label(bar, text='César · Vigenère · Sustitución + Análisis de Frecuencia', font=('Consolas', 9), bg=P['panel'], fg=P['muted'] ).pack(side=tk.RIGHT, padx=18) st = ttk.Style() st.theme_use('default') st.configure('C.TNotebook', background=P['bg'], borderwidth=0) st.configure('C.TNotebook.Tab', background=P['panel2'], foreground=P['muted'], padding=[22, 8], font=('Consolas', 10, 'bold')) st.map('C.TNotebook.Tab', background=[('selected', P['btn'])], foreground=[('selected', 'white')]) nb = ttk.Notebook(self.root, style='C.TNotebook') nb.pack(fill=tk.BOTH, expand=True, padx=12, pady=10) for tab_text, fn in [ (' César / ROT-N ', self._build_caesar), (' Vigenère ', self._build_vigenere), (' Sustitución + Frecuencias ', self._build_substitution), ]: f = tk.Frame(nb, bg=P['bg']) nb.add(f, text=tab_text) fn(f) # ══════════════════════════════════════════════════════════════════════════ # TAB 1 · CÉSAR / ROT-N # ══════════════════════════════════════════════════════════════════════════ def _build_caesar(self, parent): left = tk.Frame(parent, bg=P['bg'], padx=16, pady=12) left.pack(side=tk.LEFT, fill=tk.Y) right = tk.Frame(parent, bg=P['bg'], pady=12) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Slider desplazamiento tk.Label(left, text='Desplazamiento N', font=('Consolas', 10, 'bold'), bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) srow = tk.Frame(left, bg=P['bg']) srow.pack(fill=tk.X, pady=(4, 12)) tk.Scale(srow, from_=0, to=25, orient=tk.HORIZONTAL, variable=self.shift_var, command=self._on_shift, bg=P['panel2'], fg=P['text'], highlightthickness=0, troughcolor=P['border'], length=230, font=('Consolas', 9)).pack(side=tk.LEFT) self.c_shift_lbl = tk.Label(srow, text='N = 3', font=('Consolas', 13, 'bold'), bg=P['bg'], fg=P['hl'], padx=8) self.c_shift_lbl.pack(side=tk.LEFT) # Modo mrow = tk.Frame(left, bg=P['bg']) mrow.pack(fill=tk.X, pady=(0, 12)) for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]: tk.Radiobutton(mrow, text=txt, variable=self.cmode_var, value=val, command=self._update_caesar, bg=P['bg'], fg=col, selectcolor=P['panel2'], activebackground=P['bg'], font=('Consolas', 10, 'bold') ).pack(side=tk.LEFT, padx=(0, 18)) # Input / output for attr, lbl_txt, fg, editable in [ ('c_in', 'Texto de entrada', P['ac'], True), ('c_out', 'Resultado', P['rc'], False), ]: tk.Label(left, text=lbl_txt, font=('Consolas', 9), bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) w = tk.Text(left, height=4, width=33, font=('Consolas', 11), wrap=tk.WORD, bg=P['panel2'] if editable else P['panel'], fg=fg, relief=tk.FLAT, insertbackground=fg, state=tk.NORMAL if editable else tk.DISABLED) w.pack(pady=(2, 10)) setattr(self, attr, w) self.c_in.bind('', lambda e: self._update_caesar()) self.c_in.insert('1.0', 'Hola Mundo') self.c_formula = tk.Label(left, text='', font=('Consolas', 9), bg=P['bg'], fg=P['muted'], justify=tk.LEFT) self.c_formula.pack(anchor=tk.W) # Rueda tk.Label(right, text='Rueda de César', font=('Consolas', 10, 'bold'), bg=P['bg'], fg=P['accent']).pack() self.c_cv = tk.Canvas(right, width=430, height=430, bg=P['bg'], highlightthickness=0) self.c_cv.pack(pady=4) self._update_caesar() def _on_shift(self, val=None): self.c_shift_lbl.config(text=f'N = {self.shift_var.get()}') self._update_caesar() def _draw_wheel(self, hl_plain=None, hl_cipher=None): cv = self.c_cv cv.delete('all') shift = self.shift_var.get() cx = cy = 215 R1, R2 = 190, 145 # outer / inner ring radius T1, T2 = 172, 126 # text radii # Rings cv.create_oval(cx-R1, cy-R1, cx+R1, cy+R1, fill=P['panel2'], outline=P['border'], width=2) cv.create_oval(cx-R2, cy-R2, cx+R2, cy+R2, fill=P['panel'], outline=P['accent'], width=2) cv.create_oval(cx-105, cy-105, cx+105, cy+105, fill=P['panel2'], outline=P['border'], width=1) # Segment dividers for i in range(N): a = math.radians(-90 + i * 360/N) xa = cx + R2 * math.cos(a); ya = cy + R2 * math.sin(a) xb = cx + R1 * math.cos(a); yb = cy + R1 * math.sin(a) cv.create_line(xa, ya, xb, yb, fill=P['border'], width=1) for i in range(N): angle = math.radians(-90 + i * 360/N) # Outer: texto plano (fijo) plain_letter = ALPHA[i] xo = cx + T1 * math.cos(angle) yo = cy + T1 * math.sin(angle) is_hl = (hl_plain and plain_letter == hl_plain) if is_hl: cv.create_oval(xo-14, yo-14, xo+14, yo+14, fill='#2d2200', outline=P['hl'], width=2) cv.create_text(xo, yo, text=plain_letter, font=('Consolas', 11, 'bold'), fill=P['hl'] if is_hl else P['text']) # Inner: cifrado (girado por shift) cipher_letter = ALPHA[(i + shift) % N] xi = cx + T2 * math.cos(angle) yi = cy + T2 * math.sin(angle) is_hl2 = (hl_cipher and cipher_letter == hl_cipher) if is_hl2: cv.create_oval(xi-14, yi-14, xi+14, yi+14, fill='#0a2510', outline=P['rc'], width=2) cv.create_text(xi, yi, text=cipher_letter, font=('Consolas', 11, 'bold'), fill=P['rc'] if is_hl2 else P['bc']) # Centro cv.create_text(cx, cy - 10, text=f'N = {shift}', font=('Consolas', 14, 'bold'), fill=P['accent']) cv.create_text(cx, cy + 12, text='C = (P+N) mod 26', font=('Consolas', 8), fill=P['muted']) # Flecha indicadora en la cima tip_x = cx + (R1 + 8) * math.cos(math.radians(-90)) tip_y = cy + (R1 + 8) * math.sin(math.radians(-90)) cv.create_polygon(tip_x - 7, tip_y, tip_x + 7, tip_y, tip_x, tip_y - 18, fill=P['hl']) # Etiquetas de anillos cv.create_text(cx, cy - T1 - 22, text='Texto plano (fijo)', font=('Consolas', 8), fill=P['ac']) cv.create_text(cx, cy - T2 + 26, text='Cifrado (gira)', font=('Consolas', 8), fill=P['bc']) def _update_caesar(self, *_): text = self.c_in.get('1.0', tk.END).strip().upper() shift = self.shift_var.get() sgn = 1 if self.cmode_var.get() == 'E' else -1 result = '' for ch in text: result += ALPHA[(ALPHA.index(ch) + sgn * shift) % N] if ch in ALPHA else ch self.c_out.config(state=tk.NORMAL) self.c_out.delete('1.0', tk.END) self.c_out.insert('1.0', result) self.c_out.config(state=tk.DISABLED) first_p = next((c for c in text if c in ALPHA), None) first_c = ALPHA[(ALPHA.index(first_p) + sgn * shift) % N] if first_p else None if self.cmode_var.get() == 'E': self.c_formula.config( text=f'Fórmula: C = (P + {shift}) mod 26\n' f'Ejemplo: A(0) → {ALPHA[shift % N]}({shift})') else: self.c_formula.config( text=f'Fórmula: P = (C − {shift}) mod 26\n' f'Ejemplo: {ALPHA[shift % N]}({shift}) → A(0)') self._draw_wheel(hl_plain=first_p, hl_cipher=first_c) # ══════════════════════════════════════════════════════════════════════════ # TAB 2 · VIGENÈRE # ══════════════════════════════════════════════════════════════════════════ def _build_vigenere(self, parent): top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10) top.pack(fill=tk.X) # IO io = tk.Frame(top, bg=P['bg']) io.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18)) for attr, lbl_txt, var, fg, editable in [ ('v_plain_e', 'Texto plano', self.vig_plain, P['ac'], True), ('v_key_e', 'Clave', self.vig_key, P['bc'], True), ('v_out_e', 'Cifrado', self.vig_out, P['rc'], False), ]: tk.Label(io, text=lbl_txt, font=('Consolas', 9), bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) e = tk.Entry(io, textvariable=var, font=('Consolas', 12, 'bold'), relief=tk.FLAT, bg=P['panel2'] if editable else P['panel'], fg=fg, insertbackground=fg, width=26, state=tk.NORMAL if editable else tk.DISABLED) e.pack(pady=(2, 8), fill=tk.X) setattr(self, attr, e) self.vig_plain.trace_add('write', self._update_vig) self.vig_key.trace_add('write', self._update_vig) self.vig_plain.set('HOLA') self.v_explain = tk.Label(io, text='', font=('Consolas', 8), bg=P['bg'], fg=P['muted'], justify=tk.LEFT, wraplength=340) self.v_explain.pack(anchor=tk.W, pady=(6, 0)) # Mapa char a char map_f = tk.Frame(top, bg=P['bg']) map_f.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) tk.Label(map_f, text='Mapeo carácter a carácter', font=('Consolas', 9, 'bold'), bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) self.v_map_cv = tk.Canvas(map_f, height=100, bg=P['panel2'], highlightthickness=0) self.v_map_cv.pack(fill=tk.X, pady=(4, 0)) self.v_map_cv.bind('', lambda e: self._draw_vig_map()) # Tabla Vigenère (scrollable) tk.Label(parent, text='Tabla de Vigenère (fila=clave, columna=plano, celda=cifrado)', font=('Consolas', 8, 'bold'), bg=P['bg'], fg=P['muted'] ).pack(padx=14, anchor=tk.W) tf = tk.Frame(parent, bg=P['bg']) tf.pack(fill=tk.BOTH, expand=True, padx=14, pady=(2, 10)) self.v_tbl = tk.Canvas(tf, bg=P['panel2'], highlightthickness=0) vb = ttk.Scrollbar(tf, orient=tk.VERTICAL, command=self.v_tbl.yview) hb = ttk.Scrollbar(tf, orient=tk.HORIZONTAL, command=self.v_tbl.xview) self.v_tbl.configure(yscrollcommand=vb.set, xscrollcommand=hb.set) hb.pack(side=tk.BOTTOM, fill=tk.X) vb.pack(side=tk.RIGHT, fill=tk.Y) self.v_tbl.pack(fill=tk.BOTH, expand=True) self._draw_vig_table() def _draw_vig_table(self, hr=-1, hc=-1): cv = self.v_tbl cv.delete('all') CS = 20 for r in range(27): for c in range(27): x, y = c * CS, r * CS if r == 0 and c == 0: txt, bg, fg = '', P['panel'], P['muted'] elif r == 0: li = c - 1; hl = (li == hc) txt = ALPHA[li] bg = P['ac'] if hl else P['panel'] fg = P['bg'] if hl else P['ac'] elif c == 0: li = r - 1; hl = (li == hr) txt = ALPHA[li] bg = P['bc'] if hl else P['panel'] fg = P['bg'] if hl else P['bc'] else: letter = ALPHA[(r - 1 + c - 1) % N] active = (r-1 == hr and c-1 == hc) in_r = (r-1 == hr) in_c = (c-1 == hc) txt = letter if active: bg, fg = P['rc'], P['bg'] elif in_r: bg, fg = '#162210', P['rc'] elif in_c: bg, fg = '#101630', P['ac'] else: bg, fg = P['panel2'], P['muted'] cv.create_rectangle(x, y, x+CS, y+CS, fill=bg, outline=P['bg'], width=1) cv.create_text(x+CS//2, y+CS//2, text=txt, font=('Consolas', 8, 'bold' if r == 0 or c == 0 or (r-1==hr and c-1==hc) else 'normal'), fill=fg) total = 27 * CS cv.configure(scrollregion=(0, 0, total, total)) def _update_vig(self, *_): plain = ''.join(c for c in self.vig_plain.get().upper() if c in ALPHA) key = ''.join(c for c in self.vig_key.get().upper() if c in ALPHA) self.v_out_e.config(state=tk.NORMAL) if not plain or not key: self.vig_out.set('') self.v_out_e.config(state=tk.DISABLED) self._draw_vig_table() return cipher = [ALPHA[(ALPHA.index(p) + ALPHA.index(key[i % len(key)])) % N] for i, p in enumerate(plain)] self.vig_out.set(''.join(cipher)) self.v_out_e.config(state=tk.DISABLED) p0 = ALPHA.index(plain[0]); k0 = ALPHA.index(key[0]); c0 = (p0+k0) % N self._draw_vig_table(hr=k0, hc=p0) self.v_explain.config( text=(f"'{plain[0]}' (col {p0}) + clave '{key[0]}' (fila {k0})\n" f" → ({p0}+{k0}) mod 26 = {c0} → '{ALPHA[c0]}'")) self._vig_p = list(plain) self._vig_k = list(key) self._vig_c = cipher self._draw_vig_map() def _draw_vig_map(self): cv = self.v_map_cv cv.delete('all') pl = self._vig_p; ky = self._vig_k; ci = self._vig_c if not pl: return W = cv.winfo_width() or 500 n = min(len(pl), max(1, (W - 72) // 30)) cell = max(26, (W - 72) // max(n, 1)) for ri, (letters, color, y, label) in enumerate([ (pl[:n], P['ac'], 18, 'Plano '), (ky[:n], P['bc'], 52, 'Clave '), (ci[:n], P['rc'], 86, 'Cifrado'), ]): cv.create_text(6, y, text=label, anchor=tk.W, font=('Consolas', 8), fill=P['muted']) for j, letter in enumerate(letters): x = 66 + j * cell + cell // 2 if j == 0: cv.create_rectangle(x-14, y-12, x+14, y+12, fill=P['panel'], outline=color, width=2) cv.create_text(x, y, text=letter, font=('Consolas', 11, 'bold'), fill=color) # Marcadores de ciclo de clave if label == 'Clave ': kl = len(ky) for j in range(1, n): if j % kl == 0: xm = 66 + j * cell cv.create_line(xm, 34, xm, 70, fill=P['border'], dash=(3, 3)) cv.create_text(xm, 32, text='↺', font=('Consolas', 7), fill=P['bc']) # ══════════════════════════════════════════════════════════════════════════ # TAB 3 · SUSTITUCIÓN + FRECUENCIAS # ══════════════════════════════════════════════════════════════════════════ def _build_substitution(self, parent): top = tk.Frame(parent, bg=P['bg'], padx=14, pady=10) top.pack(fill=tk.X) left = tk.Frame(top, bg=P['bg']) left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18)) # Modo + shuffle mrow = tk.Frame(left, bg=P['bg']) mrow.pack(fill=tk.X, pady=(0, 8)) for txt, val, col in [('Cifrar', 'E', P['ac']), ('Descifrar', 'D', P['bc'])]: tk.Radiobutton(mrow, text=txt, variable=self.sub_mode, value=val, command=self._update_sub, bg=P['bg'], fg=col, selectcolor=P['panel2'], activebackground=P['bg'], font=('Consolas', 10, 'bold') ).pack(side=tk.LEFT, padx=(0, 18)) mk_btn(left, '🔀 Nueva clave aleatoria', P['btn'], self._shuffle_sub).pack(fill=tk.X, pady=(0, 10)) for attr, lbl_txt, fg, editable in [ ('s_in', 'Texto de entrada', P['ac'], True), ('s_out', 'Resultado', P['rc'], False), ]: tk.Label(left, text=lbl_txt, font=('Consolas', 9), bg=P['bg'], fg=P['muted']).pack(anchor=tk.W) w = tk.Text(left, height=4, width=36, font=('Consolas', 11), wrap=tk.WORD, bg=P['panel2'] if editable else P['panel'], fg=fg, relief=tk.FLAT, insertbackground=fg, state=tk.NORMAL if editable else tk.DISABLED) w.pack(pady=(2, 8)) setattr(self, attr, w) self.s_in.bind('', lambda e: self._update_sub()) self.s_in.insert('1.0', 'ESTAMOS APRENDIENDO CRIPTOGRAFIA') # Tabla de clave right = tk.Frame(top, bg=P['bg']) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) tk.Label(right, text='Tabla de sustitución (A→? para cada letra)', font=('Consolas', 9, 'bold'), bg=P['bg'], fg=P['accent']).pack(anchor=tk.W) self.s_key_cv = tk.Canvas(right, height=72, bg=P['panel2'], highlightthickness=0) self.s_key_cv.pack(fill=tk.X, pady=(4, 0)) self.s_key_cv.bind('', lambda e: self._draw_sub_key()) # Histograma de frecuencias tk.Label(parent, text='Análisis de Frecuencia (texto cifrado vs. español estándar)', font=('Consolas', 9, 'bold'), bg=P['bg'], fg=P['muted'] ).pack(padx=14, anchor=tk.W, pady=(6, 0)) self.s_freq_cv = tk.Canvas(parent, bg=P['panel2'], highlightthickness=0) self.s_freq_cv.pack(fill=tk.BOTH, expand=True, padx=14, pady=(4, 10)) self.s_freq_cv.bind('', lambda e: self._draw_freq()) self.root.after(120, self._update_sub) def _shuffle_sub(self): random.shuffle(self.sub_key) self._update_sub() def _update_sub(self, *_): text = self.s_in.get('1.0', tk.END).strip().upper() mode = self.sub_mode.get() fwd = {ALPHA[i]: self.sub_key[i] for i in range(N)} rev = {v: k for k, v in fwd.items()} mapping = fwd if mode == 'E' else rev result = ''.join(mapping.get(ch, ch) if ch in ALPHA else ch for ch in text) self.s_out.config(state=tk.NORMAL) self.s_out.delete('1.0', tk.END) self.s_out.insert('1.0', result) self.s_out.config(state=tk.DISABLED) self._sub_result = result self._sub_mapping = mapping self._draw_sub_key() self._draw_freq() def _draw_sub_key(self): cv = self.s_key_cv cv.delete('all') mapping = self._sub_mapping if not mapping: return W = cv.winfo_width() or 700 cw = max(20, W // N) for i, letter in enumerate(ALPHA): x = i * cw + cw // 2 mapped = mapping.get(letter, letter) cv.create_text(x, 14, text=letter, font=('Consolas', 9), fill=P['ac']) cv.create_text(x, 36, text='↓', font=('Consolas', 8), fill=P['muted']) cv.create_text(x, 58, text=mapped, font=('Consolas', 9, 'bold'), fill=P['rc'] if mapped != letter else P['muted']) def _draw_freq(self): cv = self.s_freq_cv cv.delete('all') W = cv.winfo_width() or 800 H = cv.winfo_height() or 200 if H < 60: return letters = [c for c in self._sub_result if c in ALPHA] total = max(len(letters), 1) ct_freq = {l: letters.count(l) / total * 100 for l in ALPHA} MAX_F = 16.0 pad_l = 36 pad_b = 28 ch_w = W - pad_l - 10 ch_h = H - pad_b - 20 bot = H - pad_b top_y = bot - ch_h gw = ch_w // N bw = max(3, gw // 2 - 1) # Eje Y for pct in [0, 4, 8, 12, 16]: yp = bot - int(pct / MAX_F * ch_h) cv.create_line(pad_l - 4, yp, pad_l, yp, fill=P['muted']) cv.create_text(pad_l - 6, yp, text=f'{pct}%', anchor=tk.E, font=('Consolas', 7), fill=P['muted']) # Leyenda cv.create_rectangle(pad_l, 4, pad_l + 10, 14, fill=P['bc'], outline='') cv.create_text(pad_l + 14, 9, text='Español estándar', anchor=tk.W, font=('Consolas', 7), fill=P['muted']) cv.create_rectangle(pad_l + 130, 4, pad_l + 140, 14, fill=P['rc'], outline='') cv.create_text(pad_l + 144, 9, text='Texto cifrado', anchor=tk.W, font=('Consolas', 7), fill=P['muted']) for i, letter in enumerate(ALPHA): xb = pad_l + i * gw + 2 # Barra español es_h = int(min(FREQ_ES[letter], MAX_F) / MAX_F * ch_h) cv.create_rectangle(xb, bot - es_h, xb + bw, bot, fill=P['bc'], outline='') # Barra texto cifrado ct_h = int(min(ct_freq[letter], MAX_F) / MAX_F * ch_h) cv.create_rectangle(xb + bw + 1, bot - ct_h, xb + bw * 2 + 1, bot, fill=P['rc'], outline='') cv.create_text(xb + bw, bot + 10, text=letter, font=('Consolas', 7), fill=P['muted']) # ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == '__main__': root = tk.Tk() app = CifradosClasicos(root) root.update_idletasks() w, h = root.winfo_width(), root.winfo_height() sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() root.geometry(f'{w}x{h}+{(sw-w)//2}+{(sh-h)//2}') root.mainloop()